femolacaster
Posted on October 25, 2022
In the first episode of this series, we looked at the importance of JSON, JSON databases, and RedisJSON, installing Redis Cloud, Redis Stack, and Redis Insight, and how we can store all types of data(scalar, object, array of objects) in RedisJSON. Make out some time to read that great article here if you haven’t. No doubt, we were getting an inch closer to our goal of finding perfect matches for returning inmates. Everyone could find true love after all. Let’s take a step further toward our goal in this article.
Yay!!! It’s time to create!
The wonders of RedisJSON would further be explored in this next tutorial. How can we prepare our data dimensions for our matches using code? With Golang, we would explore how to interact smoothly with our RedisJSON database and allow returning inmates to specify their interests in a breeze.
Cool stuff, yeah?!
If you are excited already, can I get an upvote? ❤️
We would be using a simple directory structure and code arrangement pattern typically in this post (as much as we can). It is recommended to use more Golang idiomatic architectural styles in more serious implementation. We would however separate concerns in the simplest of forms. We could expand on this pattern in a future post. We would also be using the REST API standard. This code would be built as a monolith to avoid complexities but can be scaled to much more advanced architectures later. To the micro-services and best practices Lords:
Let’s make a directory for our code. In UNIX-like systems, we can do:
mkdir dating-app && cd dating-app
It'd be great starting with setting and tidying up some of our dependencies. Run this in your terminal’s root project:
#go mod init {your-repo-name}
#For me I have:
go mod init github.com/femolacaster/dating-app
#Tidy things up
go mod tidy
#Call on your Redis Soldiers
go get github.com/gomodule/redigo/redis
go get github.com/nitishm/go-rejson/v4
#Let us include MUX for our API routing
go get -u github.com/gorilla/mux
The next step would be to create the following routes in a folder named routes in our application’s root directory:
[route-dir]/routes/routes.go
package routes
import (
"github.com/femolacaster/dating-app/controllers"
"github.com/gorilla/mux"
)
func Init() *mux.Router {
route := mux.NewRouter()
route.HandleFunc("/api/v1/criteria", controllers.ShowAll)
route.HandleFunc("/api/v1/criteria", controllers.Add).Methods("POST")
route.HandleFunc("/api/v1/criteria/ {id}/dimension", controllers.ShowDimension)
return route
}
A simple routing is shown in the code above. The Init function returns 3 exported routes that would allow for the new addition of Criteria for returning Inmates which uses the POST method, display all the various dating criteria of Inmates on the application, and returns the dimension of particular criteria (either Casual or Serious) using the GET method.
A good next step would be to create helpers for our code. Helpers are functions that you use repeatedly throughout your code. They come through 😊. Two helper functions identified in this case are “RenderErrorResponse” and “RenderResponse “respectively. These functions help to render the output of our API in a simple format depending on whether it is an error or otherwise.
What we have in:
[route-dir]/helpers/dating.go
package helpers
import (
"encoding/json"
"net/http"
)
type ErrorResponse struct {
Error string `json:"error"`
}
func RenderErrorResponse(w http.ResponseWriter, msg string, status int) {
RenderResponse(w, ErrorResponse{Error: msg}, status)
}
func RenderResponse(w http.ResponseWriter, res interface{}, status int) {
w.Header().Set("Content-Type", "application/json")
content, err := json.Marshal(res)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(status)
if _, err = w.Write(content); err != nil {
}
}
In short, we can add one more helper function. All it does is connect to our RedisJSON local database and output the Redigo client connection instance which we can use for our logic:
func NewRedisConn() *rejson.Handler {
var addr = flag.String("Server", "localhost:6379", "Redis server address")
rh := rejson.NewReJSONHandler()
flag.Parse()
// Redigo Client
conn, err := redis.Dial("tcp", *addr)
if err != nil {
log.Fatalf("Failed to connect to redis-server @ %s", *addr)
}
defer func() {
_, err = conn.Do("FLUSHALL")
err = conn.Close()
if err != nil {
log.Fatalf("Failed to communicate to redis-server @ %v", err)
}
}()
rh.SetRedigoClient(conn)
return rh
}
Let us create the logic for our routes.
We create a new file:
[route-dir]/controllers/dating.go
This file would have three functions that define our logic. The first would allow for the new addition of Criteria for returning Inmates, the second would display all the various dating criteria of Inmates on the application and the last would allow filtering by criteria (either Casual or Serious).
The first thing to do in this section would be to store the various interest in a struct and then embody the interest and other details to form an Inmate’s criteria as shown in this struct:
type Criteria struct {
ID int `json:"id"`
Name string `json:"name"`
Height float32 `json:"height"` //height in feet and inches
WeightKG int `json:"weight"`
SexualOrientation string `json:"sexualOrientation"`
Age int `json:"age"`
CasualInterest CasualInterest `json:"casualInterest"`
SeriousInterest SeriousInterest `json:"seriousInterest"`
}
type SeriousInterest struct {
Career bool `json:"career"`
Children bool `json:"children "`
Communication bool `json:"communication"`
Humanity bool `json:"humanity"`
Investment bool `json:"investment"`
Marriage bool `json:"marriage"`
Religion bool `json:"religion"`
Politics bool `json:"politics"`
}
type CasualInterest struct {
Entertainment bool `json:"entertainment"`
Gym bool `json:"gym"`
Jewellries bool `json:"jewellries"`
OneNight bool `json:"oneNight"`
Restaurant bool `json:"restaurant"`
Swimming bool `json:"swimming"`
Travel bool `json:"travel"`
Yolo bool `json:"yolo"`
}
In all our logic functions, we used the returned Golang rejson instance in the helpers.NewRedisConn function that will be used to communicate to our RedisJSON database.
rh := helpers.NewRedisConn()
Rejson is a Redis module that implements ECMA-404, the JSON Data Interchange Standard as a native data type and allows storing, updating, and fetching of JSON values from Redis keys which also supports the two popular Golang clients: Redigo and go-redis.
Here are the differences between Redigo and go-redis to make your own informed choice:
Redigo | Go-Redis |
---|---|
It is less type-safe | It is more type-safe |
It could be faster and easier to use | It could be slower and may not be easier to use as Redigo |
Do not use it if planning to scale your database to a high-available cluster | Perfect for clustering. Perfecto! |
So yeah, the choice is yours. In this post, we, of course, chose the easier option which is Redigo and you would be seeing its usage in the controller functions.
For our first function that adds criteria for an Inmate:
func Add(w http.ResponseWriter, r *http.Request) {
var req Criteria
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
helpers.RenderErrorResponse(w, "invalid request", http.StatusBadRequest)
return
}
defer r.Body.Close()
rh := helpers.NewRedisConn()
res, err := rh.JSONSet("criteria", ".", &req)
if err != nil {
log.Fatalf("Failed to JSONSet")
return
}
if res.(string) == "OK" {
fmt.Printf("Success: %s\n", res)
helpers.RenderResponse(w, helpers.ErrorResponse{Error: "Successfully inserted new Criteria to Database"}, http.StatusCreated)
} else {
fmt.Println("Failed to Set: ")
helpers.RenderErrorResponse(w, "invalid request", http.StatusBadRequest)
}
}
The second endpoint that shows all criteria is shown below:
func ShowAll(w http.ResponseWriter, r *http.Request) {
rh := helpers.NewRedisConn()
criteriaJSON, err := redis.Bytes(rh.JSONGet("criteria", "."))
if err != nil {
log.Fatalf(("Failed to get JSON"))
return
}
readCriteria := Criteria{}
err = json.Unmarshal(criteriaJSON, &readCriteria)
if err != nil {
fmt.Printf("JSON Unmarshal Failed")
helpers.RenderErrorResponse(w, "invalid request", http.StatusBadRequest)
}
fmt.Printf("Student read from RedisJSON:%#v\n", readCriteria)
helpers.RenderResponse(w, helpers.ErrorResponse{Error: "Successful retrieval of criterias"}, http.StatusOK)
}
Now for getting if an Inmate’s criteria are Casual or Serious, could you try implementing that yourself?
There are many ways to go about it.
A tip would be:
Get all criteria from RedisJSON just as shown in the ShowAll function but this time using the key which is the id to get those criteria. Then since the CasualInterest struct and SeriousInterest struct have fields that are bool, compare the two individual struct values to determine which has the most “1” or “true”. That way you can decide the Inmate who is tilted to looking for something serious or casual. That logic works, I guess 🤔. But of course, you could come up with much better logic.
That should be easy. Would be nice to drop some of your beautiful implementations in the comment section😀.
In the main.go on our root directory, we can create our server:
package main
import (
"errors"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/femolacaster/dating-app/routes"
"github.com/ichtrojan/thoth"
"github.com/joho/godotenv"
)
func main() {
logger, thothErr := thoth.Init("log")
if thothErr != nil {
log.Fatal(thothErr)
}
//warning, error, log, trace, metrics
if envLoadErr := godotenv.Load(); envLoadErr != nil {
logger.Log(errors.New("There was a problem loading an environmental file. Please check file is present."))
log.Fatal("Error:::There was a problem loading an environmental file. Please check file is present.")
}
appPort, appPortExist := os.LookupEnv("APPPORT")
if !appPortExist {
logger.Log(errors.New("There was no Port variable for the application in the env file"))
log.Fatal("Error:::There was no Port variable for the application in the env file")
}
address := ":" + appPort
srv := &http.Server{
Handler: routes.Init(),
Addr: address,
ReadTimeout: 1 * time.Second,
ReadHeaderTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
IdleTimeout: 1 * time.Second,
}
log.Println("Starting server", address)
fmt.Println("Go to localhost:" + appPort + " to view application")
log.Fatal(srv.ListenAndServe())
}
So, let’s get our server up:
In your project root, run the code by running this command:
go run main.go
That’s it! We have successfully set up a simple API for returning inmates to get their matches. How awesome?!
This means that any system can connect to it and make use of the database information in its means, style, etc.
Let us dig into that assertion further. Make sure you have your Redis database instance on and spring up RedisInsight to have a view into what is going on.
1) Consider a simple use case: MR Peter who was once an Inmate wishes to declare his astonishing profile showing that he has quite a lot of qualities and hopes someone would accept and love him for who he is. With our API, MR Peter can fulfill this need via a mobile client, an IoT device, his browser, etc. maybe by speaking, typing, etc. translated in this manner:
curl -X POST localhost :9000 /api/v1/criteria
-H "Content-Type: application/json"
-d ' {
"id":DATIN00025,
"name":"Mr Peter Griffin",
"height":6.4,
"weight":120,
"sexualOrientation":"straight",
"age":45,
"casualInterest":{
"entertainment":true,
"gym":false,
"jewellries":false,
"oneNight":false,
"restaurant":true,
"swimming":false,
"travel":false,
"yolo":true
},
"seriousInterest":{
"career":false,
"children ":true,
"communication":false,
"humanity":false,
"investment":false,
"marriage":false,
"religion":false,
"politics":true
}
}
‘
2) Another use case. Mrs. Lois desires to connect with someone who can understand her, who can understand what it means to be behind bars as she has also been in that situation. She needs that man with dripping masculinity and vigor. Calling our API through her client just as seen below does the magic to show her all men available for her selection:
curl localhost :9000 /api/v1/criteria
-H "Accept: application/json"
3) Miss Meg, wants both sides of the coin at a casual level. No strong strings attached. She probably wants to know whether a particular sweet match meets that need. She sees Peter Griffin’s profile earlier and wants to determine if he got some casual or serious vibes. Miss Meg presses a button on her mobile, and all her mobile has to do is to call our unimplemented showDimension endpoint for Mr. Peter Griffin to see whether he is a casual match in a similar call such as:
curl localhost :9000 /api/v1/criteria/ DATIN00025/dimension
-H "Accept: application/json"
As with these matches, Mr.Peter, Mrs. Lois and Miss. Meg have been sorted. So as many more using this wonderful API we built!
That’s it! We have been able to find the perfect matches with ease! If that ain’t magic, what then?
Now, ask yourself. Should you be a RedisJSON enthusiast?🤔
As we journey through the other series, exploring other good sides of Redis such as RediSearch in the next episodes, we would keep progressing with our idea of helping returning inmates find their true love. And maybe someday, this would be a means for them to reintegrate into society faster and better. See you in the next series.
Something great for you! If you enjoyed this article, click on the upvote icon❤️, and show some love❤️ too. Show that we share some interest already ❤️. Maybe we should date 😊. When the total number of upvotes❤️ gets to, should I say 200? I would also share the full source code on Github.
All right, love birds❤️! Enjoy😀! Upvote❤️! Comment💬! it’d go a long way.
Comment especially💬. Let’s put some ideas into this code. I know you have some great ideas there.
This post is in collaboration with Redis.
You can check the following references for ideas:
- Try Redis Cloud for free
- Watch this video on the benefits of Redis Cloud over other Redis providers
- Redis Developer Hub - tools, guides, and tutorials about Redis
- RedisInsight Desktop GUI
[Image Credits: Photo by Marcus Ganahl on Unsplash]
Posted on October 25, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.