diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..6682937 --- /dev/null +++ b/.air.toml @@ -0,0 +1,3 @@ +[build] + cmd = "go build -o ./tmp/main ./cmd/camp" + bin = "./tmp/main" \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c88bfa8 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +PORT=8080 \ No newline at end of file diff --git a/cmd/camp/main.go b/cmd/camp/main.go index 8c02566..7043881 100644 --- a/cmd/camp/main.go +++ b/cmd/camp/main.go @@ -1,14 +1,32 @@ package main import ( + "encoding/json" "log" "net/http" + "os" + "time" "github.com/codersgyan/camp/internal/contact" "github.com/codersgyan/camp/internal/database" + "github.com/joho/godotenv" + httpSwagger "github.com/swaggo/http-swagger/v2" + + _ "github.com/codersgyan/camp/docs" ) func main() { + if err := godotenv.Load(); err != nil { + log.Printf("warning: could not load .env file: %v", err) + } + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + addr := ":" + port + db, err := database.Connect("./camp_data/camp.db") if err != nil { log.Fatal(err) @@ -23,7 +41,45 @@ func main() { contactRepository := contact.NewRepository(db) contactHandler := contact.NewHandler(contactRepository) - http.HandleFunc("POST /api/contacts", contactHandler.Create) + mux := http.NewServeMux() + + // health check endpoint + mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"msg": "Hi Camp"}) + }) + + // swagger UI endpoint + mux.Handle("GET /swagger/", httpSwagger.Handler( + httpSwagger.URL("http://localhost:8080/swagger/doc.json"), + )) + + mux.HandleFunc("POST /api/contacts", contactHandler.Create) + + log.Printf("server is running on http://localhost:%s", port) + log.Printf("checkout API Documentation is running on http://localhost:%s/swagger", port) + log.Fatal(http.ListenAndServe(addr, requestLogger(mux))) +} + +type loggingResponseWriter struct { + http.ResponseWriter + statusCode int +} + +func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { + return &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK} +} + +func (lrw *loggingResponseWriter) WriteHeader(code int) { + lrw.statusCode = code + lrw.ResponseWriter.WriteHeader(code) +} - log.Fatal(http.ListenAndServe(":8080", nil)) +func requestLogger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + lrw := newLoggingResponseWriter(w) + next.ServeHTTP(lrw, r) + log.Printf("%s %s %d %s", r.Method, r.URL.Path, lrw.statusCode, time.Since(start)) + }) } diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..be1ac66 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,154 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/contacts": { + "post": { + "description": "Upsert a contact record. If the email exists, tags are updated; otherwise, a new contact is created.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Contacts" + ], + "summary": "Create or update a contact with tags", + "parameters": [ + { + "description": "Contact payload", + "name": "contact", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/contact.Contact" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "contact.Contact": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2025-11-15T00:00:00Z" + }, + "email": { + "type": "string", + "example": "ava@example.com" + }, + "first_name": { + "type": "string", + "example": "Krishna" + }, + "id": { + "type": "integer", + "example": 0 + }, + "last_name": { + "type": "string", + "example": "Web" + }, + "phone": { + "type": "string", + "example": "+1-555-0102" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/contact.Tag" + } + }, + "updated_at": { + "type": "string", + "example": "2025-11-15T00:00:00Z" + } + } + }, + "contact.Tag": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer", + "example": 0 + }, + "text": { + "type": "string", + "example": "customers" + }, + "updated_at": { + "type": "string", + "example": "2025-11-15T00:00:00Z" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..ecf02ab --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,125 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/api/contacts": { + "post": { + "description": "Upsert a contact record. If the email exists, tags are updated; otherwise, a new contact is created.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Contacts" + ], + "summary": "Create or update a contact with tags", + "parameters": [ + { + "description": "Contact payload", + "name": "contact", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/contact.Contact" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "contact.Contact": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2025-11-15T00:00:00Z" + }, + "email": { + "type": "string", + "example": "ava@example.com" + }, + "first_name": { + "type": "string", + "example": "Krishna" + }, + "id": { + "type": "integer", + "example": 0 + }, + "last_name": { + "type": "string", + "example": "Web" + }, + "phone": { + "type": "string", + "example": "+1-555-0102" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/contact.Tag" + } + }, + "updated_at": { + "type": "string", + "example": "2025-11-15T00:00:00Z" + } + } + }, + "contact.Tag": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer", + "example": 0 + }, + "text": { + "type": "string", + "example": "customers" + }, + "updated_at": { + "type": "string", + "example": "2025-11-15T00:00:00Z" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..4056092 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,85 @@ +definitions: + contact.Contact: + properties: + created_at: + example: "2025-11-15T00:00:00Z" + type: string + email: + example: ava@example.com + type: string + first_name: + example: Krishna + type: string + id: + example: 0 + type: integer + last_name: + example: Web + type: string + phone: + example: +1-555-0102 + type: string + tags: + items: + $ref: '#/definitions/contact.Tag' + type: array + updated_at: + example: "2025-11-15T00:00:00Z" + type: string + type: object + contact.Tag: + properties: + created_at: + type: string + id: + example: 0 + type: integer + text: + example: customers + type: string + updated_at: + example: "2025-11-15T00:00:00Z" + type: string + type: object +info: + contact: {} +paths: + /api/contacts: + post: + consumes: + - application/json + description: Upsert a contact record. If the email exists, tags are updated; + otherwise, a new contact is created. + parameters: + - description: Contact payload + in: body + name: contact + required: true + schema: + $ref: '#/definitions/contact.Contact' + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: + format: int64 + type: integer + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Create or update a contact with tags + tags: + - Contacts +swagger: "2.0" diff --git a/go.mod b/go.mod index 0aed561..c330968 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,23 @@ module github.com/codersgyan/camp go 1.25.2 -require github.com/mattn/go-sqlite3 v1.14.32 +require ( + github.com/mattn/go-sqlite3 v1.14.32 + github.com/swaggo/http-swagger/v2 v2.0.2 + github.com/swaggo/swag v1.16.6 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum index 66f7516..11df88d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,63 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= +github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/contact/handler.go b/internal/contact/handler.go index 7aa31c9..ee90abe 100644 --- a/internal/contact/handler.go +++ b/internal/contact/handler.go @@ -14,6 +14,17 @@ func NewHandler(repo *Repository) *Handler { return &Handler{repo: repo} } +// Create Contact godoc +// @Summary Create or update a contact with tags +// @Description Upsert a contact record. If the email exists, tags are updated; otherwise, a new contact is created. +// @Tags Contacts +// @Accept json +// @Produce json +// @Param contact body Contact true "Contact payload" +// @Success 201 {object} map[string]int64 +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/contacts [post] func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { // todo: request validation diff --git a/internal/contact/model.go b/internal/contact/model.go index 3b23d5c..189b6bf 100644 --- a/internal/contact/model.go +++ b/internal/contact/model.go @@ -19,3 +19,21 @@ type Contact struct { UpdatedAt time.Time `json:"updated_at"` Tags []Tag `json:"tags"` } + + + +/** + +{ + "first_name": "Krishna", + "last_name": "Bansal", + "email": "ava@example.com", + "phone": "+1-555-0102", + "tags": [ + { "text": "customers" }, + { "text": "newsletter" } + ] +} + +use this data + */ \ No newline at end of file diff --git a/makefile b/makefile index 02b1ef7..a1f36e5 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,9 @@ run: build @./bin/camp +# install---> go install github.com/air-verse/air@latest +dev: + @PATH=$$HOME/go/bin:$$PATH air build: @go build -o ./bin/camp cmd/camp/main.go \ No newline at end of file diff --git a/tmp/build-errors.log b/tmp/build-errors.log new file mode 100644 index 0000000..865c401 --- /dev/null +++ b/tmp/build-errors.log @@ -0,0 +1 @@ +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/tmp/main b/tmp/main new file mode 100755 index 0000000..001ec47 Binary files /dev/null and b/tmp/main differ