diff --git a/.idea/codetest.iml b/.idea/codetest.iml
new file mode 100644
index 0000000..8240566
--- /dev/null
+++ b/.idea/codetest.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/GOPATH__codetest_.xml b/.idea/libraries/GOPATH__codetest_.xml
new file mode 100644
index 0000000..b65c215
--- /dev/null
+++ b/.idea/libraries/GOPATH__codetest_.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..e8860d2
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+ ApexVCS
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..34b1a90
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..510b687
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,561 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ gameO
+ ServeMux
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ DEFINITION_ORDER
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ file://$PROJECT_DIR$/client/server/server.go
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 24c006c..165677c 100644
--- a/README.md
+++ b/README.md
@@ -38,3 +38,8 @@ ln -s `pwd`/hooks/pre-commit .git/hooks
```bash
go get -v ./... && go build -v
```
+
+# Running the client
+Available in client/ dir. `go run main.go` will use defaults that work on most systems, otherwise arguments are available.
+* `/` is a list that links to the key/ template
+* `/key/RESULT.KEY`
\ No newline at end of file
diff --git a/client/draws/consume.go b/client/draws/consume.go
new file mode 100644
index 0000000..0af983a
--- /dev/null
+++ b/client/draws/consume.go
@@ -0,0 +1,7 @@
+package draws
+
+// Fetcher fetches results
+type Fetcher interface {
+ GetAll() ([]Result, error)
+ ByKey(key string) (Result, error)
+}
diff --git a/client/draws/data.go b/client/draws/data.go
new file mode 100644
index 0000000..4ce7067
--- /dev/null
+++ b/client/draws/data.go
@@ -0,0 +1,160 @@
+package draws
+
+// Response is the response from the feed
+type Reponse struct {
+ Results []Result `json:"result"`
+ Messages []interface{} `json:"messages"`
+}
+
+// Result is the key result
+type Result struct {
+ Type string `json:"type"`
+ Key string `json:"key"`
+ Name string `json:"name"`
+ Autoplayable string `json:"autoplayable"`
+ GameTypes []GameTypes `json:"game_types,omitempty"`
+ Draws []Draws `json:"draws,omitempty"`
+ Days []Day `json:"days,omitempty"`
+ Addons []interface{} `json:"addons,omitempty"`
+ QuickpickSizes []int `json:"quickpick_sizes,omitempty"`
+ Lottery Lottery `json:"lottery,omitempty"`
+ Draw Draw `json:"draw,omitempty"`
+}
+
+// Draw is the type of draw
+type Draw struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ DrawNumber int `json:"draw_number"`
+ DrawStop string `json:"draw_stop"`
+ DrawDate string `json:"draw_date"`
+ Prize Prize `json:"prize"`
+ Offers []Offer `json:"offers"`
+ TermsAndConditionsURL string `json:"terms_and_conditions_url"`
+}
+
+// Offer is the offer details
+type Offer struct {
+ Name string `json:"name"`
+ Key string `json:"key"`
+ NumTickets int `json:"num_tickets"`
+ Price Price `json:"price"`
+ PricePerTicket Price `json:"price_per_ticket"`
+ Ribbon string `json:"ribbon"`
+ BonusPrize interface{} `json:"bonus_prize"`
+}
+
+// Price is the cost
+type Price struct {
+ Amount string `json:"amount"`
+ Currency string `json:"currency"`
+}
+
+// Prize is the details of the prize
+type Prize struct {
+ Type string `json:"type"`
+ CardTitle string `json:"card_title"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Content PrizeContent `json:"content"`
+ Value Price `json:"value"`
+ ValueIsExact bool `json:"value_is_exact"`
+ HeroImage interface{} `json:"hero_image"`
+ CarouselImages []string `json:"carousel_images"`
+ FeatureDrawImage interface{} `json:"feature_draw_image"`
+ EdmImage string `json:"edm_image"`
+}
+
+// PrizeContent is for details for customer
+type PrizeContent struct {
+ SalesPitchHeading1 string `json:"sales_pitch_heading_1"`
+ SalesPitchSubHeading1 string `json:"sales_pitch_sub_heading_1"`
+ Paragraph1 string `json:"paragraph_1"`
+ Paragraph2 string `json:"paragraph_2"`
+ Paragraph3 string `json:"paragraph_3"`
+ Image string `json:"image"`
+ SalesPitchHeading2 string `json:"sales_pitch_heading_2"`
+ SalesPitchSubHeading2 string `json:"sales_pitch_sub_heading_2"`
+ Features []string `json:"features"`
+}
+
+// Draws is the draws available
+type Draws struct {
+ Name interface{} `json:"name"`
+ Date string `json:"date"`
+ Stop string `json:"stop"`
+ DrawNo int `json:"draw_no"`
+ PrizePool Price `json:"prize_pool"`
+ JackpotImage JackpotImage `json:"jackpot_image"`
+}
+
+// JackpotImage is the image for the jackpot
+type JackpotImage struct {
+ ImageName string `json:"image_name"`
+ ImageURL string `json:"image_url"`
+ SvgURL string `json:"svg_url"`
+ ImageWidth int `json:"image_width"`
+ ImageHeight int `json:"image_height"`
+ ContentDescription string `json:"content_description"`
+}
+
+// GameTypes is the type of games
+type GameTypes struct {
+ Key string `json:"key"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ GameOffers []GameOffer `json:"game_offers"`
+}
+
+// GameOffer is the offer for the game
+type GameOffer struct {
+ Key string `json:"key"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Price Price `json:"price"`
+ MinGames int `json:"min_games"`
+ MaxGames int `json:"max_games"`
+ Multiple int `json:"multiple"`
+ Ordered bool `json:"ordered"`
+ GameIncrement GameIncrement `json:"game_increment"`
+ EquivalentGames int `json:"equivalent_games"`
+ NumberSets []NumberSet `json:"number_sets"`
+ DisplayRange interface{} `json:"display_range"`
+}
+
+// NumberSet is the number set
+type NumberSet struct {
+ First int `json:"first"`
+ Last int `json:"last"`
+ Sets []Set `json:"sets"`
+}
+
+// Set is the set
+type Set struct {
+ Name string `json:"name"`
+ Count int `json:"count"`
+}
+
+// GameIncrement is the increment for the game
+type GameIncrement struct {
+ Num4 int `json:"4"`
+}
+
+// Lottery is the lottery detials
+type Lottery struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Desc string `json:"desc"`
+ Multidraw bool `json:"multidraw"`
+ Type string `json:"type"`
+ IconURL string `json:"icon_url"`
+ IconWhiteURL string `json:"icon_white_url"`
+ PlayURL string `json:"play_url"`
+ LotteryID int `json:"lottery_id"`
+}
+
+// Day is the day for the result
+type Day struct {
+ Name string `json:"name"`
+ Value int `json:"value"`
+}
diff --git a/client/draws/live-cached.go b/client/draws/live-cached.go
new file mode 100644
index 0000000..7ccfee6
--- /dev/null
+++ b/client/draws/live-cached.go
@@ -0,0 +1,73 @@
+package draws
+
+import (
+ "fmt"
+ "github.com/sirupsen/logrus"
+ "sync"
+ "time"
+)
+
+// Cache caches another Fetchers results - if more complex data feed, or larger data set, an ARC mechanism might be useful. E.g. hashicorps
+type Cache struct {
+ sync.RWMutex
+ Feed Fetcher
+ Cache map[string]Result
+ Logger *logrus.Logger
+}
+
+// Update updates the cache
+func (c Cache) Update() error {
+ res, err := c.Feed.GetAll()
+ if err != nil {
+ return err
+ }
+ c.Lock()
+ for _, r := range res {
+ c.Cache[r.Key] = r
+ }
+ c.Unlock()
+ return nil
+}
+
+// Monitor ticks every 30 seconds to trigger a cache update. This could be done with a retry-backoff algorithm as well.
+func (c Cache) Monitor() {
+ for range time.Tick(time.Second * 30) {
+ err := c.Update()
+ if err != nil {
+ c.Logger.Error(err)
+ }
+ }
+}
+
+// GetAll returns all Results in the cache
+func (c Cache) GetAll() (res []Result, err error) {
+ c.RLock()
+ for _, r := range c.Cache {
+ res = append(res, r)
+ }
+ c.RUnlock()
+ return
+}
+
+// ByKey returns a result by key
+func (c Cache) ByKey(key string) (res Result, err error) {
+ c.RLock()
+ defer c.RUnlock()
+ res, ok := c.Cache[key]
+ if !ok {
+ err = fmt.Errorf("No result found for key: %s", key)
+ }
+ return
+}
+
+// NewCachedFeed returns a cached feed fetcher
+func NewCachedFeed(feed Fetcher, logger *logrus.Logger) Fetcher {
+ c := Cache{
+ Cache: map[string]Result{},
+ Feed: feed,
+ Logger: logger,
+ }
+ c.Update()
+ go c.Monitor()
+ return c
+}
diff --git a/client/draws/live.go b/client/draws/live.go
new file mode 100644
index 0000000..c31f90f
--- /dev/null
+++ b/client/draws/live.go
@@ -0,0 +1,62 @@
+package draws
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/sirupsen/logrus"
+ "net/http"
+)
+
+// ResultsLiveFeed is a fetcher that works directly on the external feed
+type ResultsLiveFeed struct {
+ FeedServerPath string
+ Logger *logrus.Logger
+}
+
+// getData fetches the data from the feed
+func (r ResultsLiveFeed) getData() (jsonData Reponse, err error) {
+ resp, err := http.Get(r.FeedServerPath)
+ if err != nil {
+ return
+ }
+
+ jsonData = Reponse{}
+ err = json.NewDecoder(resp.Body).Decode(&jsonData)
+ return
+}
+
+// GetAll returns all results
+func (r ResultsLiveFeed) GetAll() (res []Result, err error) {
+ jsonData, err := r.getData()
+ res = jsonData.Results
+ return
+}
+
+// ByKey returns a Result by key
+func (r ResultsLiveFeed) ByKey(key string) (res Result, err error) {
+ resp, err := http.Get(r.FeedServerPath)
+ if err != nil {
+ return
+ }
+
+ jsonData := Reponse{}
+ err = json.NewDecoder(resp.Body).Decode(&jsonData)
+ if err != nil {
+ return
+ }
+
+ for _, r := range jsonData.Results {
+ if r.Key == key {
+ return r, nil
+ }
+ }
+ return Result{}, fmt.Errorf("No result found for key: %s", key)
+}
+
+// NewLiveFeed returns a new live feed fetcher
+func NewLiveFeed(feedServerPath string, logger *logrus.Logger) Fetcher {
+ return ResultsLiveFeed{
+ FeedServerPath: feedServerPath,
+ Logger: logger,
+ }
+}
diff --git a/client/main.go b/client/main.go
new file mode 100644
index 0000000..f299cf1
--- /dev/null
+++ b/client/main.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "github.com/fromz/codetest/client/draws"
+ "github.com/fromz/codetest/client/server"
+ "github.com/sirupsen/logrus"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+ "html/template"
+ "net/http"
+ "os"
+ "path/filepath"
+)
+
+func main() {
+ // Pass a logger around for centralized logging if necessary
+ logger := logrus.New()
+
+ // Viper allows us to migrate to YAML or TOML files fairly easily
+ pflag.String("templatePath", "templates", "The path to the templates directory")
+ pflag.String("feedServerPath", "http://127.0.0.1:80/", "The feed server address")
+ pflag.String("bindTo", ":8081", "The TCP info to bind to")
+ pflag.Bool("cacheFeed", true, "Whether or not to locally cache the feed")
+ pflag.Parse()
+ err := viper.BindPFlags(pflag.CommandLine)
+ if err != nil {
+ logger.Error(err)
+ os.Exit(1)
+ }
+
+ // Bind the passed in config to local variables
+ templatePath := viper.GetString("templatePath")
+ feedServerPath := viper.GetString("feedServerPath")
+ bindTo := viper.GetString("bindTo")
+ cacheFeed := viper.GetBool("cacheFeed")
+
+ // Parse the error template in advance in case the error its attempting to display is due to templates not being found
+ t, err := template.ParseFiles(filepath.Join(templatePath, "error-500.html.tmpl"))
+ if err != nil {
+ logger.Error(err)
+ os.Exit(1)
+ }
+
+ // Prepare a Draws fetcher for use by the HTML server
+ fetcher := draws.NewLiveFeed(feedServerPath, logger)
+ if cacheFeed {
+ fetcher = draws.NewCachedFeed(fetcher, logger)
+ }
+
+ // Prepare a server and register its methods as handlers. if more complicated, I'd utilize a framework.
+ server.RegisterLogger(logger)
+ s := server.HTML{
+ Fetcher: fetcher,
+ TemplatePath: templatePath,
+ ParsedErrorTemplate: t,
+ }
+
+ h := http.NewServeMux()
+ h.HandleFunc("/", s.List)
+ h.HandleFunc("/key/", s.Key)
+ logger.Infof("Listening on: %s", bindTo)
+ err = http.ListenAndServe(bindTo, server.Logger(h))
+ if err != nil {
+ logger.WithError(err).Errorf("Error binding to: %s", bindTo)
+ }
+}
diff --git a/client/server/logger.go b/client/server/logger.go
new file mode 100644
index 0000000..95573ef
--- /dev/null
+++ b/client/server/logger.go
@@ -0,0 +1,21 @@
+package server
+
+import (
+ "github.com/sirupsen/logrus"
+ "net/http"
+)
+
+var logger *logrus.Logger
+
+// RegisterLogger makes logrus available locally
+func RegisterLogger(logrus *logrus.Logger) {
+ logger = logrus
+}
+
+// Logger wrapper
+func Logger(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ logger.Infof("Serving request %s", r.URL.Path)
+ h.ServeHTTP(w, r)
+ })
+}
diff --git a/client/server/server.go b/client/server/server.go
new file mode 100644
index 0000000..6e817d0
--- /dev/null
+++ b/client/server/server.go
@@ -0,0 +1,91 @@
+package server
+
+import (
+ "github.com/davecgh/go-spew/spew"
+ "github.com/fromz/codetest/client/draws"
+ "github.com/sirupsen/logrus"
+ "html/template"
+ "net/http"
+ "path/filepath"
+ "sort"
+)
+
+// HTML is the server type for HTML
+type HTML struct {
+ Fetcher draws.Fetcher
+ // TODO implement a cache for parsed templates, with a goroutine monitoring either inotify refreshing the templates, or every x seconds rebuild
+ TemplatePath string
+ ParsedErrorTemplate *template.Template
+ Logger *logrus.Logger
+}
+
+// ListData is the data for the List template
+type ListData struct {
+ Results []draws.Result
+}
+
+// List renders the list template
+func (s HTML) List(w http.ResponseWriter, r *http.Request) {
+ res, err := s.Fetcher.GetAll()
+ if err != nil {
+ s.Error500(w, r, err, "")
+ return
+ }
+
+ // Sort alphabetically
+ sort.Slice(res, func(i, j int) bool {
+ return res[i].Name < res[j].Name
+ })
+
+ t, err := template.ParseFiles(filepath.Join(s.TemplatePath, "list.html.tmpl"))
+ if err != nil {
+ s.Error500(w, r, err, "")
+ return
+ }
+
+ t.Execute(w, ListData{
+ Results: res,
+ })
+}
+
+// KeyData is the data for the Key template
+type KeyData struct {
+ Dump string
+ Result draws.Result
+}
+
+// Key renders the Key template
+func (s HTML) Key(w http.ResponseWriter, r *http.Request) {
+ key := r.URL.Path[len("/key/"):]
+ res, err := s.Fetcher.ByKey(key)
+ if err != nil {
+ s.Error500(w, r, err, "")
+ return
+ }
+
+ str := spew.Sdump(res)
+
+ t, err := template.ParseFiles(filepath.Join(s.TemplatePath, "by-key.html.tmpl"))
+ if err != nil {
+ s.Error500(w, r, err, "")
+ return
+ }
+
+ t.Execute(w, KeyData{
+ Dump: str,
+ Result: res,
+ })
+}
+
+// ErrorData is the data for the error template
+type ErrorData struct {
+ Message string
+}
+
+// Error500 renders the error 500 template
+func (s HTML) Error500(w http.ResponseWriter, r *http.Request, err error, msg string) {
+ s.Logger.WithError(err).Errorf("Serving URL %s", r.URL.Path)
+ s.ParsedErrorTemplate.Execute(w, ErrorData{
+ Message: msg,
+ })
+}
diff --git a/client/templates/by-key.html.tmpl b/client/templates/by-key.html.tmpl
new file mode 100644
index 0000000..800adea
--- /dev/null
+++ b/client/templates/by-key.html.tmpl
@@ -0,0 +1,10 @@
+
+
+
+
+ {{ .Result.Name }}
+
+
+{{ .Dump }}
+
+
\ No newline at end of file
diff --git a/client/templates/error-500.html.tmpl b/client/templates/error-500.html.tmpl
new file mode 100644
index 0000000..f8a1d77
--- /dev/null
+++ b/client/templates/error-500.html.tmpl
@@ -0,0 +1,12 @@
+
+
+
+
+ ERROR
+
+
+
+{{ .Message }}
+
+
+
\ No newline at end of file
diff --git a/client/templates/list.html.tmpl b/client/templates/list.html.tmpl
new file mode 100644
index 0000000..d333383
--- /dev/null
+++ b/client/templates/list.html.tmpl
@@ -0,0 +1,14 @@
+
+
+
+
+ List of draws
+
+
+
+{{ range $key, $value := .Results }}
+{{ $value.Name }}
+{{ end }}
+
+
+
\ No newline at end of file
diff --git a/main.go b/main.go
index 1a496d3..a454ec8 100644
--- a/main.go
+++ b/main.go
@@ -15,6 +15,7 @@ func baseHandler(w http.ResponseWriter, r *http.Request) {
w.Write(fileContents)
}
+// This probably needs work too
func main() {
r := mux.NewRouter()
log.Println("Waiting...")