Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Marcy #16

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 181 additions & 36 deletions controllers/library.go
Original file line number Diff line number Diff line change
@@ -1,85 +1,230 @@
package controllers

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strconv"

"acme-books/models"
"cloud.google.com/go/datastore"
"github.com/go-martini/martini"
"google.golang.org/api/iterator"

"acme-books/models"
)

type LibraryController struct{}
type LibraryController struct {
ctx context.Context
client *datastore.Client
}

func (lc LibraryController) GetByKey(params martini.Params, w http.ResponseWriter) {
ctx := context.Background()
client, _ := datastore.NewClient(ctx, "acme-books")
func NewLibraryController() *LibraryController {
lc := new(LibraryController)
lc.ctx = context.Background()
lc.client, _ = datastore.NewClient(lc.ctx, "acme-books")

defer client.Close()
books := []models.Book{
{Id: 1, Author: "George Orwell", Title: "1984", Borrowed: false},
{Id: 2, Author: "George Orwell", Title: "Animal Farm", Borrowed: false},
{Id: 3, Author: "Robert Jordan", Title: "Eye of the world", Borrowed: false},
{Id: 4, Author: "Various", Title: "Collins Dictionary", Borrowed: false},
}

id, err := strconv.Atoi(params["id"])
var keys []*datastore.Key

if err != nil {
for _, book := range books {
keys = append(keys, datastore.IDKey("Book", book.Id, nil))
}

if _, err := lc.client.PutMulti(lc.ctx, keys, books); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}

var book models.Book
key := datastore.IDKey("Book", int64(id), nil)
return lc
}
func (lc LibraryController) Close() error {
return lc.client.Close()
}

err = client.Get(ctx, key, &book)
func (lc LibraryController) GetByKey(params martini.Params, w http.ResponseWriter) {

if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
id, err := strconv.Atoi(params["id"])
if handleNoneNilErr(err, w, http.StatusBadRequest) {
return
}

jsonStr, err := json.MarshalIndent(book, "", " ")

if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
key := datastore.IDKey("Book", int64(id), nil)
var book models.Book
err = lc.client.Get(lc.ctx, key, &book)
if handleNoneNilErr(err, w, http.StatusInternalServerError) {
return
}

w.WriteHeader(http.StatusOK)
w.Write(jsonStr)
writeJson(book, w)
}

func (lc LibraryController) ListAll(r *http.Request, w http.ResponseWriter) {
ctx := context.Background()
client, _ := datastore.NewClient(ctx, "acme-books")

defer client.Close()
filter := r.URL.Query().Get("q")

var output []models.Book
query := datastore.NewQuery("Book")

if filter != "" {
splitIndices := regexp.MustCompile("(<=|<|>=|>|=)").FindStringIndex(filter)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be wary of over engineering. This is an impressive bit of code, but it would leave you open to running queries like borrowed > false which doesn't really make much sense. Sometimes simpler is better 😅

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just seen the commit message for 3. An incredibly over-engineered way to do Filtering 😄 . As an aside, what would you have done to simplify this? Ie lock it down so you could only filter on the title or author. And it was a string contains match?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quickest way to modify the method to only allow strings would be to return a 400 in place of converting the filter string to bool or int.

The simplest code though would be to remove the type checking completely, since before that was added the queries would return nothing if the column type wasn't string (as it would be like doing WHERE Id = "3", which doesn't match the int 3), but that doesn't allow any kind of handling, it just returns empty.

The main reason it's so complicated is because I didn't want to redefine the book model anywhere, so I had to make it dynamically find the type to convert the parameter correctly. If I threw out that requirement (since it's not actually in the exercise), I could just hard code a switch-case statement to check which name is in "fieldName", then either convert based on type or essentially make a passlist that only allows author and title. That would cut 47 lines down to like 12 and be less over-engineered

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very sensible answer! How about the match being a contains match, rather than an equals?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to do that

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats fine. Datastore doesn't support contains (or case in-sensitive matches amongst other things). So it was a bit of a trick question (sorry about that)

You would most likely do a Get all from the datastore, then https://pkg.go.dev/strings#Contains check each one in turn. If we know its going to be a small list (100's) and not run often (less than once a minute say) then we would be ok with this brute force approach. If it was more performant, then indexes or tech like big query/elastic search might be more approriate

if len(splitIndices) < 2 {
w.WriteHeader(http.StatusBadRequest)
return
}
sepIndex := splitIndices[1]
fieldName := filter[:splitIndices[0]]
filterStr := filter[:sepIndex]

metaQuery := datastore.NewQuery("__property__")
type Prop struct {
Reps []string `datastore:"property_representation"`
}
var props []Prop

keys, err := lc.client.GetAll(lc.ctx, metaQuery, &props)
if handleNoneNilErr(err, w, http.StatusBadRequest) {
return
}

var columnKind string
ok := false
for i, k := range keys {
if k.Name == fieldName {
columnKind = props[i].Reps[0]
ok = true
break
}
}
if !ok {
w.WriteHeader(http.StatusBadRequest)
return
}

var filterVal interface{}
switch columnKind {
case "BOOLEAN":
filterVal, err = strconv.ParseBool(filter[sepIndex:])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful with these methods. If this failed, then filterVal would be false/0 (the default's in go for bool and int is false/0, not nil), and err would be an error. But you aren't checking the err before running the filter, so you wouldn't get the results you expect at this point

case "INT64":
filterVal, err = strconv.Atoi(filter[sepIndex:])
default:
filterVal = filter[sepIndex:]
}

query = query.Filter(filterStr, filterVal)

}

query = query.Order("Id")
it := lc.client.Run(lc.ctx, query)

it := client.Run(ctx, datastore.NewQuery("Book"))
for {
var b models.Book
_, err := it.Next(&b)
if err == iterator.Done {
if err != nil {
fmt.Println(err)
break
}
output = append(output, b)
}

jsonStr, err := json.MarshalIndent(output, "", " ")
writeJson(output, w)
}

if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
func (lc LibraryController) Borrow(params martini.Params, w http.ResponseWriter) {
lc.SetBorrowed(params, w, true)
return
}

func (lc LibraryController) Return(params martini.Params, w http.ResponseWriter) {
lc.SetBorrowed(params, w, false)
return
}
func (lc LibraryController) SetBorrowed(params martini.Params, w http.ResponseWriter, borrowed bool) {
id, err := strconv.Atoi(params["id"])
if handleNoneNilErr(err, w, http.StatusBadRequest) {
return
}
key := datastore.IDKey("Book", int64(id), nil)

var book models.Book
err = lc.client.Get(lc.ctx, key, &book)
if handleNoneNilErr(err, w, http.StatusBadRequest) {
return
}
if book.Borrowed == borrowed {
w.WriteHeader(http.StatusBadRequest)
return
}
book.Borrowed = borrowed
_, err = lc.client.Mutate(lc.ctx, datastore.NewUpdate(key, &book))
if handleNoneNilErr(err, w, http.StatusBadRequest) {
return
}
w.WriteHeader(http.StatusNoContent)
return
}

func (lc LibraryController) New(r *http.Request, w http.ResponseWriter) {
var book models.Book
body := new(bytes.Buffer)

_, err := io.Copy(body, r.Body)
if handleNoneNilErr(err, w, http.StatusBadRequest) {
return
}

err = json.Unmarshal(body.Bytes(), &book)
if handleNoneNilErr(err, w, http.StatusBadRequest) {
return
}

key := datastore.IncompleteKey("Book", nil)
key, err = lc.client.Put(lc.ctx, key, &book)
if handleNoneNilErr(err, w, http.StatusBadRequest) {
return
}

type bookWithKey struct {
Key string
models.Book
}
amendedBook := *new(bookWithKey)
amendedBook.Key = (*key).String()
amendedBook.Book = book

jsonStr, err := json.MarshalIndent(amendedBook, "", " ")
if handleNoneNilErr(err, w, http.StatusBadRequest) {
return
}

w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonStr)
handleNoneNilErr(err, w, http.StatusInternalServerError)
}

func writeJson(item interface{}, w http.ResponseWriter) {
jsonStr, err := json.MarshalIndent(item, "", " ")

if handleNoneNilErr(err, w, http.StatusInternalServerError) {
return
}

w.WriteHeader(http.StatusOK)
w.Write(jsonStr)
_, err = w.Write(jsonStr)
handleNoneNilErr(err, w, http.StatusInternalServerError)
}

func handleNoneNilErr(err error, w http.ResponseWriter, httpResponseStatusCode int) bool {
if err != nil {
fmt.Println(err)
w.WriteHeader(httpResponseStatusCode)
return true
}
return false
}
26 changes: 25 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,32 @@ go 1.17

require (
cloud.google.com/go/datastore v1.6.0
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab
github.com/joho/godotenv v1.4.0
google.golang.org/api v0.60.0
)

require (
cloud.google.com/go v0.97.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.2.1 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 // indirect
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed // indirect
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0 // indirect
github.com/envoyproxy/protoc-gen-validate v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 // indirect
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 // indirect
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211021150943-2b146023228c // indirect
google.golang.org/grpc v1.40.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)
Loading