<repo_name>bayansar/AdventureWorkReview<file_sep>/sql/migration.sql
ALTER TABLE adventureworks.productreview
  CHANGE ReviewDate ReviewDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CHANGE ModifiedDate ModifiedDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  ADD CONSTRAINT fk_product_id FOREIGN KEY (ProductID) REFERENCES adventureworks.product(ProductID),
  ALTER COLUMN Rating SET DEFAULT 0,
  ADD Status varchar(255);<file_sep>/mysql/review.go
package mysql

import (
	"log"
	"time"
	"fmt"

	"github.com/astaxie/beego/orm"
	_ "github.com/go-sql-driver/mysql"
	"github.com/bayansar/AdventureWorkReview/review"
)

type ReviewDbService struct {
	orm orm.Ormer
}

func NewReviewDbService(username, password, dbName, host string) *ReviewDbService {
	orm.RegisterModel(new(review.Review))

	err := orm.RegisterDataBase(
		"default",
		"mysql",
		fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=utf8", username, password, host, dbName), 30)
	if err != nil {

	}
	orm.RunSyncdb("default", false, true)

	return &ReviewDbService{
		orm: orm.NewOrm(),
	}
}

func (rds *ReviewDbService) Insert(review *review.Review) (int64, error) {
	review.CreatedAt = time.Now()
	review.LastUpdateAt = time.Now()
	review.Status = "SUBMITTED"
	id, err := rds.orm.Insert(review)
	if err != nil {
		return 0, err
	}
	log.Printf("new review is created in db with id: %d,", review.ID)
	return id, nil
}

func (rds *ReviewDbService) Update(review *review.Review) (int64, error) {
	num, err := rds.orm.Update(review)
	if err != nil {
		log.Printf("%s: %s", "Failed to update a review", err)
		return 0, err
	}
	log.Printf("the review is updated in db with id: %d,", review.ID)
	return num, nil
}

func (rds *ReviewDbService) GetById(id int64) (*review.Review, error) {
	r := &review.Review{
		ID: id,
	}
	err := rds.orm.Read(r)
	if err != nil {
		return nil, err
	}
	return r, nil
}

func (rds *ReviewDbService) GetApprovedReviews() ([]*review.Review, error) {
	var reviews []*review.Review
	_, err := rds.orm.QueryTable("productreview").Filter("status", "APPROVED").All(&reviews)
	if err != nil {
		return nil, err
	}
	return reviews, nil
}
<file_sep>/review/review.go
package review

import (
	"time"
)

type Review struct {
	ID           int64     `json:"id" orm:"auto;column(ProductReviewID)"`
	ProductId    string    `json:"productid" orm:"column(ProductID)"`
	Name         string    `json:"name" orm:"column(ReviewerName)"`
	CreatedAt    time.Time `json:"createdAt" orm:"null;column(ReviewDate)"`
	Email        string    `json:"email" orm:"column(EmailAddress)"`
	Review       string    `json:"review" orm:"column(Comments)"`
	Status       string    `json:"status" orm:"null;column(Status)"`
	LastUpdateAt time.Time `json:"lastUpdateAt" orm:"null;column(ModifiedDate)"`
}

func (u *Review) TableName() string {
	return "productreview"
}

type QueueService interface {
	Publish(review *Review) error
	Subscribe() (<-chan Review, error)
}

type DbService interface {
	Insert(review *Review) (int64, error)
	Update(review *Review) (int64, error)
	GetById(id int64) (*Review, error)
	GetApprovedReviews() ([]*Review, error)
}

type NotifyService interface {
	Notify(review *Review, message string) error
}
<file_sep>/review/notifier.go
package review

import (
	"log"
)

type Notifier struct {
	ConsumerQueue QueueService
	NotifyService NotifyService
	approveMes    string
	declineMes    string
}

func (n *Notifier) Run() error {
	n.approveMes = "Your review is approved and released!"
	n.declineMes = "Your review is declined because of bad language!"

	messages, err := n.ConsumerQueue.Subscribe()
	if err != nil {
		return err
	}

	go func() {
		for m := range messages {
			var notifyMes string
			if m.Status == "APPROVED" {
				notifyMes = n.approveMes
			} else if m.Status == "DECLINED" {
				notifyMes = n.declineMes
			} else {
				log.Printf("%s: %s", "Illegal status to notify which is ", m.Status)
			}
			err := n.NotifyService.Notify(&m, notifyMes)
			if err != nil {
				log.Printf("%s: %s", "Failed to notify user", err)
			}
		}
	}()

	return nil
}
<file_sep>/review/validator.go
package review

import (
	"log"
	"regexp"
	"strings"
)

type Validator struct {
	PublisherQueue QueueService
	ConsumerQueue  QueueService
	DB             DbService
	BadWords       []string
}

func (v *Validator) Run() error {
	reg, err := regexp.Compile("[^a-zA-Z0-9]+")
	if err != nil {
		log.Printf("%s: %s", "Failed compile regex", err)
		return err
	}

	messages, err := v.ConsumerQueue.Subscribe()
	if err != nil {
		return err
	}

	go func() {
		for m := range messages {
			wordList := v.split(m.Review, reg)
			if v.checkForBadWords(wordList, v.BadWords) == true {
				m.Status = "APPROVED"
			} else {
				m.Status = "DECLINED"
			}
			_, err := v.DB.Update(&m)
			if err != nil {
				continue
			}
			err = v.PublisherQueue.Publish(&m)
			if err != nil {
				continue
			}
		}
	}()

	return nil
}

func (v *Validator) split(review string, reg *regexp.Regexp) []string {
	processedReview := reg.ReplaceAllString(review, " ")
	return strings.Split(processedReview, " ")
}

func (v *Validator) checkForBadWords(wordList []string, badWords []string) bool {
	// A map might be used in case of high number of bad words to avoid from quadratic complexity
	for _, word := range wordList {
		for _, badWord := range v.BadWords {
			if strings.EqualFold(word, badWord) == true {
				return false
			}
		}
	}
	return true
}
<file_sep>/review/api.go
package review

import (
	"log"
	"net/http"
	"fmt"
	"encoding/json"
	"strconv"
	"github.com/gorilla/mux"
)

type Api struct {
	Queue QueueService
	DB    DbService
}

func (a *Api) CreateReview() http.HandlerFunc {
	return errorHandler(func(w http.ResponseWriter, r *http.Request) error {
		defer r.Body.Close()

		newReview := Review{}
		if err := json.NewDecoder(r.Body).Decode(&newReview); err != nil {
			return fmt.Errorf("cannot decoding review : %v", err)
		}

		id, err := a.DB.Insert(&newReview)
		if err != nil {
			return err
		}

		newReview.ID = id
		err = a.Queue.Publish(&newReview)
		if err != nil {
			return err
		}

		err = json.NewEncoder(w).Encode(map[string]string{
			"success":  "true",
			"reviewId": strconv.FormatInt(newReview.ID, 10),
		})
		if err != nil {
			return err
		}
		return nil
	})
}

func (a *Api) GetReviewById() http.HandlerFunc {
	return errorHandler(func(w http.ResponseWriter, r *http.Request) error {
		vars := mux.Vars(r)
		idStr := vars["id"]
		id, err := strconv.ParseInt(idStr, 10, 64)
		if err != nil {
			return fmt.Errorf("cannot parse variable: %v", err)
		}
		review, err := a.DB.GetById(id)
		if err != nil {
			return err
		}
		err = json.NewEncoder(w).Encode(review)
		if err != nil {
			return err
		}
		return nil
	})
}

func (a *Api) GetApprovedReviews() http.HandlerFunc {
	return errorHandler(func(w http.ResponseWriter, r *http.Request) error {
		reviews, err := a.DB.GetApprovedReviews()
		if err != nil {
			return nil
		}
		err = json.NewEncoder(w).Encode(reviews)
		if err != nil {
			return err
		}
		return nil
	})
}

func errorHandler(f func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		err := f(w, r)
		if err != nil {
			log.Printf("handling %q: %v", r.RequestURI, err)
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}
<file_sep>/README.md
# AdventureWorkReview

## **Instructions**

1. *Clone the repository*
2. *Download docker images*
    ```
   docker pull mysql
   docker pull rabbitmq:3-management-alpine
   ``` 
3. *Change current folder to project*
    ``` 
   cd AdventureWorkReview
   ``` 
4. *Build docker image*
    ``` 
   docker-compose build
   ```
5. *Create and start containers*
    ``` 
   docker-compose up -d
   ```
   - Please wait several minutes for migrating database. When it is complete, the application container would start.
   - You can check whether it starts with the command below:
        ``` 
       docker ps
        ```
6. *Post a review*
    ``` 
    curl -X POST http://0.0.0.0:8888/api/reviews \ -H 'Content-Type: application/json' \
    -d '{
    "name": "<NAME>",
    "email": "<EMAIL>", "productid": "3",
    "review": "I really love the product and will recommend!" }'
    ``` 
    
    
    
## **REST API**
``` 
- POST  '/api/reviews'          (Create a new review)
- GET   '/api/reviews/approved' (Get all approved reviews)
- GET   '/api/reviews/{id}'     (Get a review)    
``` 
<file_sep>/Dockerfile
FROM golang:1.9 as builder

# Set go bin which doesn't appear to be set already.
ENV GOBIN /go/bin

RUN go get -u github.com/golang/dep/...

COPY . ${GOPATH}/src/github.com/bayansar/AdventureWorkReview/
WORKDIR ${GOPATH}/src/github.com/bayansar/AdventureWorkReview/

# Go dep!
RUN dep ensure -vendor-only && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /review-app

RUN go test -v

# STEP 2 build a small image
FROM scratch

ENV RABBIT_URI amqp://guest:guest@rabbitmq:5672
ENV MYSQL_USER root
ENV MYSQL_PASSWORD <PASSWORD>
ENV DB_NAME=adventureworks
ENV VALIDATE_QUEUE_NAME=validate
ENV NOTIFY_QUEUE_NAME=notify
ENV BAD_WORDS=fee,nee,cruul,leent
ENV DB_HOST=mysql

COPY --from=builder /review-app /review-app
EXPOSE 8888

CMD ["/review-app"]<file_sep>/main.go
package main

import (
	"log"
	"net/http"

	"github.com/caarlos0/env"
	"github.com/gorilla/mux"

	"github.com/bayansar/AdventureWorkReview/email"
	"github.com/bayansar/AdventureWorkReview/mysql"
	"github.com/bayansar/AdventureWorkReview/rabbitmq"
	"github.com/bayansar/AdventureWorkReview/review"
	"os"
)

type Config struct {
	RabbitUri         string   `env:"RABBIT_URI,required"`
	DbHost            string   `env:"DB_HOST,required"`
	DbUser            string   `env:"MYSQL_USER,required"`
	DbPassword        string   `env:"MYSQL_PASSWORD,required"`
	DbName            string   `env:"DB_NAME,required"`
	ValidateQueueName string   `env:"VALIDATE_QUEUE_NAME,required"`
	NotifyQueueName   string   `env:"NOTIFY_QUEUE_NAME,required"`
	BadWords          []string `env:"BAD_WORDS,required" envSeparator:","`
}

func main() {

	cfg := Config{}
	err := env.Parse(&cfg)
	if err != nil {
		log.Fatalf("%s : %v","Couldn't parse environment variables!" , err)
	}

	reviewValidateQueueService := rabbitmq.NewReviewQueueService(cfg.RabbitUri, cfg.ValidateQueueName)
	reviewNotifyQueueService := rabbitmq.NewReviewQueueService(cfg.RabbitUri, cfg.NotifyQueueName)
	reviewDbService := mysql.NewReviewDbService(cfg.DbUser, cfg.DbPassword, cfg.DbName, cfg.DbHost)
	notifyService := email.NewReviewEmailService()

	reviewApi := &review.Api{
		Queue: reviewValidateQueueService,
		DB:    reviewDbService,
	}

	reviewValidator := &review.Validator{
		ConsumerQueue:  reviewValidateQueueService,
		PublisherQueue: reviewNotifyQueueService,
		DB:             reviewDbService,
		BadWords:       cfg.BadWords,
	}
	err = reviewValidator.Run()
	if err != nil {
		os.Exit(1)
	}

	reviewNotifier := &review.Notifier{
		ConsumerQueue: reviewNotifyQueueService,
		NotifyService: notifyService,
	}
	err = reviewNotifier.Run()
	if err != nil {
		os.Exit(1)
	}

	r := mux.NewRouter()
	r.HandleFunc("/api/reviews", reviewApi.CreateReview()).Methods("POST")
	r.HandleFunc("/api/reviews/approved", reviewApi.GetApprovedReviews()).Methods("GET")
	r.HandleFunc("/api/reviews/{id}", reviewApi.GetReviewById()).Methods("GET")

	log.Println("Server listening on port 8888...")
	log.Fatal(http.ListenAndServe(":8888", r))
}
<file_sep>/email/review.go
package email

import (
	"log"
	"github.com/bayansar/AdventureWorkReview/review"
)

type ReviewEmailService struct {
	emailService interface{}
}

func NewReviewEmailService() *ReviewEmailService{
	return &ReviewEmailService{}
}

func (res *ReviewEmailService) Notify(review *review.Review, message string) error {

	/*
	*** Send email is simulated below line***
	res.emailService.sendEmail(review.Name, review.Email, message)
	*/
	log.Println("email is sent successfully")
	return nil
}
<file_sep>/rabbitmq/review.go
package rabbitmq

import (
	"encoding/json"
	"github.com/bayansar/AdventureWorkReview/review"
	"github.com/pkg/errors"
	"github.com/streadway/amqp"
	"log"
)

type ReviewQueueService struct {
	conn *amqp.Connection
	ch   *amqp.Channel
	q    amqp.Queue
}

func NewReviewQueueService(uri string, qname string) (*ReviewQueueService, error) {
	conn, err := amqp.Dial(uri)
	if err != nil {
		//log.Printf("%s: %s", "Failed to connect to RabbitMQ", err)
		return nil, errors.Wrap(err, "Failed to connect to RabbitMQ")
	}

	ch, err := conn.Channel()
	if err != nil {
		log.Printf("%s: %s", "Failed to open a channel", err)
	}

	q, err := ch.QueueDeclare(
		qname, // name
		false, // durable
		false, // delete when unused
		false, // exclusive
		false, // no-wait
		nil,   // arguments
	)
	if err != nil {
		log.Printf("%s: %s", "Failed to declare a queue", err)
	}

	return &ReviewQueueService{
		conn: conn,
		ch:   ch,
		q:    q,
	},nil
}

func (rqs *ReviewQueueService) Publish(review *review.Review) error {
	b, err := json.Marshal(review)
	if err != nil {
		log.Printf("%s: %s", "Failed to serialize review object!", err)
		return err
	}
	err = rqs.ch.Publish(
		"",         // exchange
		rqs.q.Name, // routing key
		false,      // mandatory
		false,      // immediate
		amqp.Publishing{
			ContentType: "application/json",
			Body:        b,
		})

	if err != nil {
		log.Printf("%s: %s", "Failed to publish a message", err)
	}
	log.Printf("the review is pushed to queue: %s with id: %d,", rqs.q.Name, review.ID)
	return err
}

func (rqs *ReviewQueueService) Subscribe() (<-chan review.Review, error) {
	messages, err := rqs.ch.Consume(
		rqs.q.Name, // queue
		"",         // consumer
		true,       // auto-ack
		false,      // exclusive
		false,      // no-local
		false,      // no-wait
		nil,        // args
	)

	if err != nil {
		log.Printf("%s: %s", "Failed to register a consumer", err)
		return nil, err
	}

	reviewMessages := make(chan review.Review)

	go func() {
		for m := range messages {
			r := review.Review{}
			err := json.Unmarshal(m.Body, &r)
			if err != nil {
				log.Printf("%s: %s", "Failed to deserialize review", err)
				continue
			}
			log.Printf("review is retrieved from queue: %s  with id: %d,", rqs.q.Name, r.ID)
			reviewMessages <- r
		}
	}()

	return reviewMessages, nil
}
<file_sep>/docker-compose.yml
version: '3'
services:
  rabbitmq:
    image: rabbitmq:3-management-alpine
    restart: unless-stopped
    ports:
      - 5672:5672
      - 15672:15672

  mysql:
    image: mysql:latest
    volumes:
      - ./sql:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: <PASSWORD>
    restart: unless-stopped
    ports:
      - 3306:3306

  review-app:
    build: ./
    image: review-app:1.0
    environment:
      RABBIT_URI: amqp://guest:guest@rabbitmq:5672
      MYSQL_USER: root
      MYSQL_PASSWORD: <PASSWORD>
      DB_NAME: adventureworks
      VALIDATE_QUEUE_NAME: validate
      NOTIFY_QUEUE_NAME: notify
      BAD_WORDS: fee,nee,cruul,leent
      DB_HOST: mysql
    restart: unless-stopped
    ports:
      - 8888:8888
    links:
      - rabbitmq:rabbitmq
      - mysql:mysql
    depends_on:
      - mysql
      - rabbitmq
