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

Add questions and responses user endpoints #1

Merged
merged 14 commits into from
Sep 7, 2018
Merged

Add questions and responses user endpoints #1

merged 14 commits into from
Sep 7, 2018

Conversation

tenyo
Copy link
Contributor

@tenyo tenyo commented Sep 4, 2018

This initial PR adds basic functionality for vending questions/answers and calculating data types and risk level based on a response.
Configuration is exclusively via JSON files in S3.

log.Println(err.Error(), input)
}
return nil, err
}
Copy link

Choose a reason for hiding this comment

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

This is a little convoluted, maybe something more like

if err != nil {
    log.Println(err.Error(), input)
    if aerr, ok := err.(awserr.Error); ok && aerr == s3.ErrCodeNoSuchKey {
        return []byte{}, err
    }
    return nil, err
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

💯

log.Println(err.Error(), input)
}
return nil, err
}
Copy link

Choose a reason for hiding this comment

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

same here

actions/app.go Outdated
app.GET("/", HomeHandler)
// initialize S3 client session
S3 = s3.NewSession(os.Getenv("S3_API_KEY"), os.Getenv("S3_API_SECRET"), os.Getenv("S3_REGION"))
S3.Bucket = os.Getenv("S3_BUCKET")
Copy link

Choose a reason for hiding this comment

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

This is fine, but why not have the single constructor that also takes the bucket? Or pass the bucket as parameters to the methods that use the client?

actions/app.go Outdated
S3 = s3.NewSession(os.Getenv("S3_API_KEY"), os.Getenv("S3_API_SECRET"), os.Getenv("S3_REGION"))
S3.Bucket = os.Getenv("S3_BUCKET")

app.GET("/v1/proctor/ping", PingPong)
Copy link

Choose a reason for hiding this comment

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

This is part of the API group actually -- I only split mine in Tweaser because there was different auth middleware

}

// RiskLevelsList is a versioned collection of RiskLevels
type RiskLevelsList struct {
Copy link

Choose a reason for hiding this comment

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

This should just be RiskLevels

}

return d, nil
}
Copy link

Choose a reason for hiding this comment

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

I think this stuff needs to be abstracted into an interface the more I think about it. Let's just pull these few methods out into a helpers package for now? Then you can call helpers.LoadRiskLevels(version)


// loadRisklevels loads the risk levels json from S3 and returns a slice of bytes
func loadRiskLevels(version string) ([]byte, error) {
if len(version) == 0 {
Copy link

Choose a reason for hiding this comment

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

I think this should just be if version == ""

}
}

log.Println("Loading risk levels version " + version)
Copy link

Choose a reason for hiding this comment

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

log.Printf("Loading risk levels version %s", verison) 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

heh, still getting used to Printf 😃


log.Println("Loading risk levels version " + version)

d, err := S3.GetObject("risklevels/" + version + "/risklevels.json")
Copy link

Choose a reason for hiding this comment

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

path := fmt.Sprintf("risklevels/%s/risklevels.json", version)
d, err := S3.GetObject(path)

return []byte{}, errors.New("Object not found in S3")
}
return nil, errors.New("Unable to get object from S3")
}
Copy link

Choose a reason for hiding this comment

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

I wonder if we are over processing the error states... do you really want to throw a 500 and a stack trace if you get an error getting the object? Or would it be better to just always return []byte{}, err ? This code gets a lot simpler in the latter case (and more Go-y I think).

if err != nil {
     return []byte{}, err
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to return a 404 if object doesn't exist, I think that makes more sense than 500

Copy link

Choose a reason for hiding this comment

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

im saying if you get an error from that method, just always return a 404

versions = append(versions, s[len(s)-1])
}
return versions, nil
}
Copy link

Choose a reason for hiding this comment

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

Shouldn't this just be a method on RiskLevels?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's also used for getting the question versions.

Copy link

Choose a reason for hiding this comment

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

Ah! perfect! your first interface! 😆

if len(vl) == 0 {
return ""
}
sort.Strings(vl)
Copy link

Choose a reason for hiding this comment

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

I'm not sure this works... I think you need to convert these to numbers, no? What about the slice:

["0.1", "0.2", "0.3", ....., "0.20, 0.21", ...., "0.30", "0.31"]

Copy link

Choose a reason for hiding this comment

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

also your comment needs quotes around the version numbers if they are strings 😸

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point, I had the version as a float initially but then changed it to string to make it more flexible. i've gotta fix that sort algo.

}

// QuestionsList is a versioned collection of questions
type QuestionsList struct {
Copy link

Choose a reason for hiding this comment

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

Questions

}

var ql *QuestionsList
if err := json.Unmarshal(q, &ql); err != nil {
Copy link

Choose a reason for hiding this comment

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

This is a double pointer

@fishnix
Copy link

fishnix commented Sep 4, 2018

Nice start! 🥂

// latestVersion gets a slice of versions and returns the latest version as a string
// e.g. ["0.1", "0.10", "1.0"] or ["0.1.0", "0.10.1", "1.0.2"]
// any number of minor/patch versions will work as long as all versions are consistent
// i.e. this is not allowed ["0.1", "0.10.0", "1.0"] and the function will return ""
Copy link

Choose a reason for hiding this comment

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

💩

// LatestVersion gets a slice of versions and returns the latest version as a string
// e.g. ["0.1", "0.10", "1.0"] or ["0.1.0", "0.10.1", "1.0.2"]
// any number of minor/patch versions will work as long as all versions are consistent
// i.e. this is not allowed ["0.1", "0.10.0", "1.0"] and the function will return ""
Copy link

Choose a reason for hiding this comment

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

💩

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤷‍♂️

// e.g. ["0.1", "0.10", "1.0"] or ["0.1.0", "0.10.1", "1.0.2"]
// any number of minor/patch versions will work as long as all versions are consistent
// i.e. this is not allowed ["0.1", "0.10.0", "1.0"] and the function will return ""
func LatestVersion(vl []string) string {
Copy link

Choose a reason for hiding this comment

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

probably should return error too so you can bail from your errors below more gracefully

// i.e. this is not allowed ["0.1", "0.10.0", "1.0"] and the function will return ""
func LatestVersion(vl []string) string {
if len(vl) == 0 {
return ""
Copy link

Choose a reason for hiding this comment

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

should this actually return an error?

}

return strings.Join(ss[len(ss)-1], ".")
}
Copy link

Choose a reason for hiding this comment

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

I didn't make it take arbitrary length strings, although that should be easy.... this is quite a bit faster

➜  tmp go test -bench=. -benchtime=20s
goos: darwin
goarch: amd64
BenchmarkLatestVersionT-8   	10000000	      3858 ns/op
BenchmarkLatestVersionC-8   	10000000	      2741 ns/op
BenchmarkLatestVersionN-8   	10000000	      2436 ns/op
func LVS(vl []string) string {
	if len(vl) == 0 {
		return ""
	}

	// initialize latest with zeros
	latest := []int{}
	for i := 0; i < len(strings.Split(vl[0], ".")); i++ {
		latest = append(latest, 0)
	}

	// loop over list and compare each to latest
	for _, sv := range vl {
		current := strings.Split(sv, ".")
		if len(current) == 0 {
			log.Printf("Error: bad current version")
			return ""
		}

		si := []int{}
		for _, s := range current {
			i, err := strconv.Atoi(s)
			if err != nil {
				log.Println("Failed to parse", current)
				return ""
			}
			si = append(si, i)
		}

		if len(latest) != len(si) {
			log.Println("Error format mismatch for", sv)
			return ""
		}

		for i, siv := range si {
			if siv > latest[i] {
				latest = si
				break
			}
		}
	}

	l := []byte{}
	for _, i := range latest {
		l = strconv.AppendInt(l, int64(i), 10)
	}
	return string(l) 
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm, yeah, no need to sort all the versions if we only need to pick the latest one


// Responses is a collection of responses to questions submitted by the client
type Responses struct {
List map[string]string `json:"responses"`
Copy link

Choose a reason for hiding this comment

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

this is kinda strange -- to have the JSON key be responses

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why? have any suggestions?

}

return nil
}
Copy link

Choose a reason for hiding this comment

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

I'm thinking that these Load() methods should actually be on the S3 client and you can pass a pointer to the struct that should be filled from the storage location. Later, this could become an interface for a storage construct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it will have to be an interface, otherwise how are we going to pass a pointer to different structs?

@fishnix
Copy link

fishnix commented Sep 6, 2018

lookin good! 👍

@tenyo
Copy link
Contributor Author

tenyo commented Sep 7, 2018

I've added an S3 Load method that takes any struct which satisfies the Loader interface.

}

return nil
}
Copy link

Choose a reason for hiding this comment

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

lol now you have 3 Load() methods! Make this take interface{} instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I assumed that wasn't gonna work without even trying it 😁 Much better now!

}
}
}

return strings.Join(ss[len(ss)-1], "."), nil
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(latest)), "."), "[]"), nil
Copy link

Choose a reason for hiding this comment

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

a lot of slow functions chained together here... even a straight append instead is a lot better

package main

import(
	"fmt"
	"strings"
	"strconv"
	"testing"
)

func IntSliceString1(nums []int, separator string) string {
	return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(nums)), separator), "[]")
}

func IntSliceString2(nums []int, separator string) string {
	if len(nums) == 0 {
		return ""
	}

	s := make([]string, len(nums))
	for i, n := range nums {
		s[i] = strconv.Itoa(n)
	}

	return strings.Join(s, separator)
}

func IntSliceString3(nums []int, separator string) string {
	if len(nums) == 0 {
		return ""
	}

	s := []string{}
	for _, n := range nums {
		s = append(s, strconv.Itoa(n))
	}

	return strings.Join(s, separator)
}

func Benchmark1(b *testing.B) {
	foo := []int{1,2,3,4,5,6,7,8,9,10,100,1000}
	for n := 0; n < b.N; n++ {
		IntSliceString1(foo, ".")
	}
}

func Benchmark2(b *testing.B) {
	foo := []int{1,2,3,4,5,6,7,8,9,10,100,1000}
	for n := 0; n < b.N; n++ {
		IntSliceString2(foo, ".")
	}
}

func Benchmark3(b *testing.B) {
	foo := []int{1,2,3,4,5,6,7,8,9,10,100,1000}
	for n := 0; n < b.N; n++ {
		IntSliceString3(foo, ".")
	}
}
% ➜  tmp go test -bench=. -benchtime=20s
goos: darwin
goarch: amd64
Benchmark1-8   	20000000	      1950 ns/op
Benchmark2-8   	100000000	       417 ns/op
Benchmark3-8   	50000000	       727 ns/op
PASS
ok  	_/Users/fish/tmp/go/yale/tmp	120.277s

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but it's a one-liner 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the benchmark stuff is pretty cool!

@fishnix
Copy link

fishnix commented Sep 7, 2018

This is gettin goooooood 🤔 😆

@tenyo tenyo merged commit 8978a49 into master Sep 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants