Send Index Requests
Let's add the API for indexing books, then we can put in some test data for later use.
Create domain/gateway/book_manager.go:
/*
Package gateway contains all domain gateways.
*/
package gateway
import (
"context"
"literank.com/fulltext-books/domain/model"
)
// BookManager manages all books
type BookManager interface {
IndexBook(ctx context.Context, b *model.Book) (string, error)
}
Again, we‘re using 4 Layers Architecture Pattern. It will pay off when your project grows bigger and bigger. Read more here.
Create infrastructure/search/es.go:
/*
Package search does all search engine related implementations.
*/
package search
import (
"context"
"github.com/elastic/go-elasticsearch/v8"
"literank.com/fulltext-books/domain/model"
)
const INDEX_BOOK = "book_idx"
// ElasticSearchEngine runs all index/search operations
type ElasticSearchEngine struct {
client *elasticsearch.TypedClient
pageSize int
}
// NewEngine constructs a new ElasticSearchEngine
func NewEngine(address string, pageSize int) (*ElasticSearchEngine, error) {
cfg := elasticsearch.Config{
Addresses: []string{address},
}
client, err := elasticsearch.NewTypedClient(cfg)
if err != nil {
return nil, err
}
// Create the index
return &ElasticSearchEngine{client, pageSize}, nil
}
// IndexBook indexes a new book
func (s *ElasticSearchEngine) IndexBook(ctx context.Context, b *model.Book) (string, error) {
resp, err := s.client.Index(INDEX_BOOK).
Request(b).
Do(ctx)
if err != nil {
return "", err
}
return resp.Id_, nil
}
We‘re using the fully-typed API client (elasticsearch.TypedClient
) to index the document.
By default, Elasticsearch allows you to index documents into an index that doesn‘t exist yet. When you index a document into a non-existent index, Elasticsearch will dynamically create the index for you with default settings. This can be convenient for certain use cases where you don‘t want to manage index creation explicitly.
Install yaml
dependencies:
go get -u gopkg.in/yaml.v3
Create infrastructure/config/config.go:
/*
Package config provides config structures and parse funcs.
*/
package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// Config is the global configuration.
type Config struct {
App ApplicationConfig `json:"app" yaml:"app"`
Search SearchConfig `json:"search" yaml:"search"`
}
// SearchConfig is the configuration of search engines.
type SearchConfig struct {
Address string `json:"address" yaml:"address"`
}
// ApplicationConfig is the configuration of main app.
type ApplicationConfig struct {
Port int `json:"port" yaml:"port"`
PageSize int `json:"page_size" yaml:"page_size"`
}
// Parse parses config file and returns a Config.
func Parse(filename string) (*Config, error) {
buf, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
c := &Config{}
err = yaml.Unmarshal(buf, c)
if err != nil {
return nil, fmt.Errorf("failed to parse file %s: %v", filename, err)
}
return c, nil
}
Create config.yml:
app:
port: 8080
page_size: 10
search:
address: "http://localhost:9200"
Caution: Do not directly track your config.yml file with Git, as this could potentially leak sensitive data. If necessary, only track the template of the configuration file.
e.g.app: port: 8080 search: address: ""
Create application/executor/book_operator.go:
/*
Package executor handles request-response style business logic.
*/
package executor
import (
"context"
"literank.com/fulltext-books/domain/gateway"
"literank.com/fulltext-books/domain/model"
)
// BookOperator handles book input/output and proxies operations to the book manager.
type BookOperator struct {
bookManager gateway.BookManager
}
// NewBookOperator constructs a new BookOperator
func NewBookOperator(b gateway.BookManager) *BookOperator {
return &BookOperator{bookManager: b}
}
// CreateBook creates a new book
func (o *BookOperator) CreateBook(ctx context.Context, b *model.Book) (string, error) {
return o.bookManager.IndexBook(ctx, b)
}
Create application/wire_helper.go:
/*
Package application provides all common structures and functions of the application layer.
*/
package application
import (
"literank.com/fulltext-books/domain/gateway"
"literank.com/fulltext-books/infrastructure/config"
"literank.com/fulltext-books/infrastructure/search"
)
// WireHelper is the helper for dependency injection
type WireHelper struct {
engine *search.ElasticSearchEngine
}
// NewWireHelper constructs a new WireHelper
func NewWireHelper(c *config.Config) (*WireHelper, error) {
engine, err := search.NewEngine(c.Search.Address, c.App.PageSize)
if err != nil {
return nil, err
}
return &WireHelper{engine}, nil
}
// BookManager returns an instance of BookManager
func (w *WireHelper) BookManager() gateway.BookManager {
return w.engine
}
Create adapter/router.go:
/*
Package adapter adapts to all kinds of framework or protocols.
*/
package adapter
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"literank.com/fulltext-books/application"
"literank.com/fulltext-books/application/executor"
"literank.com/fulltext-books/domain/model"
)
// RestHandler handles all restful requests
type RestHandler struct {
bookOperator *executor.BookOperator
}
func newRestHandler(wireHelper *application.WireHelper) *RestHandler {
return &RestHandler{
bookOperator: executor.NewBookOperator(wireHelper.BookManager()),
}
}
// MakeRouter makes the main router
func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
rest := newRestHandler(wireHelper)
// Create a new Gin router
r := gin.Default()
r.POST("/books", rest.createBook)
return r, nil
}
// Create a new book
func (r *RestHandler) createBook(c *gin.Context) {
var reqBody model.Book
if err := c.ShouldBindJSON(&reqBody); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
bookID, err := r.bookOperator.CreateBook(c, &reqBody)
if err != nil {
fmt.Printf("Failed to create: %v\n", err)
c.JSON(http.StatusNotFound, gin.H{"error": "failed to create"})
return
}
c.JSON(http.StatusCreated, gin.H{"id": bookID})
}
Replace main.go with the following code:
package main
import (
"fmt"
"literank.com/fulltext-books/adapter"
"literank.com/fulltext-books/application"
"literank.com/fulltext-books/infrastructure/config"
)
const configFileName = "config.yml"
func main() {
// Read the config
c, err := config.Parse(configFileName)
if err != nil {
panic(err)
}
// Prepare dependencies
wireHelper, err := application.NewWireHelper(c)
if err != nil {
panic(err)
}
// Build main router
r, err := adapter.MakeRouter(wireHelper)
if err != nil {
panic(err)
}
// Run the server on the specified port
if err := r.Run(fmt.Sprintf(":%d", c.App.Port)); err != nil {
panic(err)
}
}
Run go mod tidy
to tidy the go.mod
file:
go mod tidy
Run the server again and try it with curl
:
go run main.go
Example request:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Da Vinci Code","author":"Dan Brown","published_at":"2003-03-18","content":"In the Louvre, a curator is found dead. Next to his body, an enigmatic message. It is the beginning of a race to discover the truth about the Holy Grail."}' \
http://localhost:8080/books
Example response:
{"id":"C1Ok9I4BexLIwExhUXKX"}
Put in more test data
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Harry Potter and the Philosopher\u0027s Stone","author":"J.K. Rowling","published_at":"1997-06-26","content":"A young boy discovers he is a wizard and begins his education at Hogwarts School of Witchcraft and Wizardry, where he uncovers the mystery of the Philosopher‘s Stone."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"To Kill a Mockingbird","author":"Harper Lee","published_at":"1960-07-11","content":"Set in the American South during the Great Depression, the novel explores themes of racial injustice and moral growth through the eyes of young Scout Finch."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Lord of the Rings","author":"J.R.R. Tolkien","published_at":"1954-07-29","content":"A hobbit named Frodo Baggins embarks on a perilous journey to destroy a powerful ring and save Middle-earth from the Dark Lord Sauron."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Catcher in the Rye","author":"J.D. Salinger","published_at":"1951-07-16","content":"Holden Caulfield narrates his experiences in New York City after being expelled from prep school, grappling with themes of alienation, identity, and innocence."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Alchemist","author":"Paulo Coelho","published_at":"1988-01-01","content":"Santiago, a shepherd boy, travels from Spain to Egypt in search of a treasure buried near the Pyramids. Along the way, he learns about the importance of following one‘s dreams."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Hunger Games","author":"Suzanne Collins","published_at":"2008-09-14","content":"In a dystopian future, teenagers are forced to participate in a televised death match called the Hunger Games. Katniss Everdeen volunteers to take her sister‘s place and becomes a symbol of rebellion."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"1984","author":"George Orwell","published_at":"1949-06-08","content":"Winston Smith lives in a totalitarian society ruled by the Party led by Big Brother. He rebels against the oppressive regime but ultimately succumbs to its control."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Girl with the Dragon Tattoo","author":"Stieg Larsson","published_at":"2005-08-01","content":"Journalist Mikael Blomkvist and hacker Lisbeth Salander investigate the disappearance of a young woman from a wealthy family, uncovering dark secrets and corruption."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Gone Girl","author":"Gillian Flynn","published_at":"2012-06-05","content":"On their fifth wedding anniversary, Nick Dunne‘s wife, Amy, disappears. As the media circus ensues and suspicions mount, Nick finds himself in a whirlwind of deception and betrayal."}' \
http://localhost:8080/books