2

I'm using Go for the first time and am trying to create a web application that takes in a receipt using json, and then generates a unique id for that receipt as well as allocates some "points" to that receipt based on some criteria. However when I enter in my json receipt into my form and hit "submit" I get the following error: invalid character 'r' looking for beginning of value.

I've printed out my receipt after it should get populated and it's empty so I'm aware that I'm just not serializing it properly. However I don't know how to serialize the json properly using go.

Here is my main.go

package main

import (
    "encoding/json"
    "fmt"
    "html/template"
    "math"
    "math/rand"
    "net/http"
    "strconv"
    "strings"
)

type Receipt struct {
    Retailer     string `json:"retailer"`
    PurchaseDate string `json:"purchaseDate"`
    PurchaseTime string `json:"purchaseTime"`
    Items        []Item `json:"items"`
    Total        string `json:"total"`
}

type Item struct {
    ShortDescription string `json:"shortDescription"`
    Price            string `json:"price"`
}

type ReceiptResult struct {
    ID     string
    Points int
}

func main() {
    http.HandleFunc("/", receiptHandler)
    http.ListenAndServe(":8080", nil)
}

func receiptHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        var receipt Receipt
        err := json.NewDecoder(r.Body).Decode(&receipt)
        j, _ := json.MarshalIndent(receipt, "", "    ")
        fmt.Println(string(j))
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }

        result := processReceipt(receipt)

        tmpl := template.Must(template.ParseFiles("result.html"))
        tmpl.Execute(w, result)
    } else {
        tmpl := template.Must(template.ParseFiles("index.html"))
        tmpl.Execute(w, nil)
    }
}

func processReceipt(receipt Receipt) ReceiptResult {
    var result ReceiptResult

    result.ID = generateID()
    result.Points += len(strings.ReplaceAll(receipt.Retailer, " ", ""))
    result.Points += roundDollarPoints(receipt.Total)
    result.Points += quarterPoints(receipt.Total)
    result.Points += itemPoints(receipt.Items)
    result.Points += itemDescriptionPoints(receipt.Items)

    return result
}

func generateID() string {
    const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    b := make([]byte, 10)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

func roundDollarPoints(total string) int {
    f, err := strconv.ParseFloat(total, 64)
    if err != nil {
        return 0
    }
    if math.Mod(f, 1) == 0 {
        return 50
    }
    return 0
}

func quarterPoints(total string) int {
    f, err := strconv.ParseFloat(total, 64)
    if err != nil {
        return 0
    }
    if math.Mod(f*100, 25) == 0 {
        return 25
    }
    return 0
}

func itemPoints(items []Item) int {
    return len(items) / 2 * 5
}

func itemDescriptionPoints(items []Item) int {
    var points int
    for _, item := range items {
        trimmedLength := len(strings.TrimSpace(item.ShortDescription))
        if trimmedLength%3 == 0 {
            price, err := strconv.ParseFloat(item.Price, 64)
            if err != nil {
                continue
            }
            points += int(math.Ceil(price * 0.2))
        }
    }
    return points
}

Here is my index.html, which is my home page

<!DOCTYPE html>
<html>
<head>
    <title>Receipt Processor</title>
</head>
<body>
    <h1>Receipt Processor</h1>
    <form method="POST" action="/">
        <label for="receipt">Receipt JSON:</label>
        <textarea id="receipt" name="receipt" rows="10" cols="50"></textarea><br><br>
        <input type="submit" value="Process Receipt">
    </form>
</body>
</html>

Here is my result.html which is the page that I should get sent to after submitting my json

<!DOCTYPE html>
<html>
<head>
    <title>Receipt Processor Result</title>
</head>
<body>
    <h1>Receipt Processor Result</h1>
    <p>Receipt ID: {{.ID}}</p>
    <p>Points Earned: {{.Points}}</p>
</body>
</html>

This is the json I've been using for testing:

{
  "retailer": "M&M Corner Market",
  "purchaseDate": "2022-03-20",
  "purchaseTime": "14:33",
  "items": [
    {
      "shortDescription": "Gatorade",
      "price": "2.25"
    },
    {
      "shortDescription": "Doritos",
      "price": "1.99"
    },
    {
      "shortDescription": "Hershey's Chocolate Bar",
      "price": "1.50"
    },
    {
      "shortDescription": "Coca-Cola",
      "price": "2.25"
    }
  ],
  "total": "8.99"
}

icza
  • 389,944
  • 63
  • 907
  • 827
cad545
  • 33
  • 2

1 Answers1

0

When you press Process Receipt button on the HTML page and the form gets POSTed, the browser will send the form data using an HTTP POST request, where the body will be the URL encoded form data, something like this:

receipt=%7B%0D%0A++%22retailer%22%3A+%22M%26M+Corner+Market%22%2C%0D%0A++%22purchaseDate%22%3A+%222022-03-20...

The body will not be the content of the Receipt text area.

On the server side, in your handler, to get Receipt text area's value, use Request.FormValue(), passing the form input's name:

input := r.FormValue("receipt")

This returns a string, so you may use strings.NewReader() that you may pass to json.NewDecoder():

    input := r.FormValue("receipt")
    var receipt Receipt
    err := json.NewDecoder(strings.NewReader(input)).Decode(&receipt)

With this modification, output will be:

{
    "retailer": "M\u0026M Corner Market",
    "purchaseDate": "2022-03-20",
    "purchaseTime": "14:33",
    "items": [
        {
            "shortDescription": "Gatorade",
            "price": "2.25"
        },
        {
            "shortDescription": "Doritos",
            "price": "1.99"
        },
        {
            "shortDescription": "Hershey's Chocolate Bar",
            "price": "1.50"
        },
        {
            "shortDescription": "Coca-Cola",
            "price": "2.25"
        }
    ],
    "total": "8.99"
}

And HTML response document:

Receipt Processor Result

Receipt ID: GUaaGCCGFs

Points Earned: 26

Also, I know this is just an example, but do not parse templates in the handler. Parse them on app startup, and just execute them in the handlers. See It takes too much time when using "template" package to generate a dynamic web page to client in Golang

Also note that your browser will also generate a /favicon.ico request that will be directed to receiptHandler() too, but it won't cause a trouble here.

icza
  • 389,944
  • 63
  • 907
  • 827