diff --git a/README.md b/README.md index f1c342f65..57f1b7dba 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,20 @@ # Real Image Challenge 2016 + +## Solution + +The solution has been completed as per specification and the solution has been hosted in render.com +[https://challenge2016.onrender.com/](https://challenge2016.onrender.com/) + +It is hosted in free tier, so there will be a warm-up time close to 1 min. After the initial warm-up it would +provide a fast response. + +The endpoints are described in the OpenApi Spec (./openapispec_v1.yaml). The same can be viewed using [https://editor.swagger.io/](https://editor.swagger.io/) + +I am looking forward to do a detailed write-up about the approch. + + + In the cinema business, a feature film is usually provided to a regional distributor based on a contract for exhibition in a particular geographical territory. Each authorization is specified by a combination of included and excluded regions. For example, a distributor might be authorzied in the following manner: diff --git a/config/config.go b/config/config.go new file mode 100644 index 000000000..9bb6c971a --- /dev/null +++ b/config/config.go @@ -0,0 +1,21 @@ +package config + +type ConifgService interface { + GetConfig(...string) *Config +} + +type ( + Config struct { + Data Data `yaml:"data"` + HttpServer HttpServer `yaml:"httpserver"` + } + + Data struct { + FilePath string `yaml:"filepath"` + } + + HttpServer struct { + Host string `yaml:"host"` + Port string `yaml:"port"` + } +) diff --git a/config/getConfig.go b/config/getConfig.go new file mode 100644 index 000000000..b102ccee6 --- /dev/null +++ b/config/getConfig.go @@ -0,0 +1,37 @@ +package config + +import ( + "os" + + "github.com/ghanithan/challenge2016/instrumentation" + "gopkg.in/yaml.v3" +) + +func GetConfig(logger instrumentation.GoLogger, args ...string) (*Config, error) { + //init config struct + config := &Config{} + + // set default file path + filePath := "../setting/sample.yaml" + // collect the filepath from varidac arguments if provided + if len(args) > 0 { + filePath = args[0] + } + logger.Info(filePath) + + // read the file + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + // unmarshal the yaml file into conifg struct + decoder := yaml.NewDecoder(file) + decoder.KnownFields(true) + if err := decoder.Decode(&config); err != nil { + return nil, err + } + + return config, nil +} diff --git a/config/getConfig_test.go b/config/getConfig_test.go new file mode 100644 index 000000000..2217591f5 --- /dev/null +++ b/config/getConfig_test.go @@ -0,0 +1,32 @@ +package config + +import ( + "reflect" + "testing" + + "github.com/ghanithan/challenge2016/instrumentation" +) + +func TestGetConfig(t *testing.T) { + t.Run( + "Testing getConfig", + func(t *testing.T) { + want := &Config{ + Data: Data{ + FilePath: "../cities.csv", + }, + } + + logger := instrumentation.InitInstruments() + + got, err := GetConfig(logger) + if err != nil { + t.Fatalf("Error in fetching config: %s", err) + } + if !reflect.DeepEqual(want, got) { + t.Fatalf("expected %q got %q", want, got) + + } + }, + ) +} diff --git a/dma/dma.go b/dma/dma.go new file mode 100644 index 000000000..f3551c988 --- /dev/null +++ b/dma/dma.go @@ -0,0 +1,831 @@ +package dma + +import ( + "encoding/json" + "fmt" + "strings" + "sync" + "time" + + "github.com/ghanithan/challenge2016/config" + "github.com/ghanithan/challenge2016/instrumentation" + loadcsv "github.com/ghanithan/challenge2016/loadcsv" + "github.com/google/uuid" +) + +// Package to handle the Designated Market Area + +// Defines the number of heirarchy used in representing DMA +// Here we have Country -> State -> City +const HEIRARCHY = 3 + +// Enum to represent the Heirarchy +const ( + CITY = iota + STATE + COUNTRY +) + +const ( + distributorAlreadyPresentError = "distributor is already present for the place" + distributorAlreadyExcludedError = "distributor is already excluded for the place" +) + +// DMA is structured as follows +type Dma struct { + sync.RWMutex + data Data + lookup Lookup + updatedTime time.Time +} + +type Data struct { + places *Place + distributors *Distributor +} + +type Lookup struct { + places map[string]*Place + distributors map[string]*Distributor +} + +//----------------------------------------------------------------------------- +// Tier - Enum to represent the Herirarchy among places +//----------------------------------------------------------------------------- + +type Tier int + +func (tier Tier) MarshalJSON() ([]byte, error) { + return json.Marshal(tier.String()) +} + +func (tier Tier) String() string { + str := "" + switch int(tier) { + case CITY: + str = "City" + case STATE: + str = "State" + case COUNTRY: + str = "Country" + default: + str = "City" + } + return str +} + +//----------------------------------------------------------------------------- +// Place - Struct to handle the places +//----------------------------------------------------------------------------- + +type Place struct { + Id uuid.UUID `json:"id"` + Type Tier `json:"type"` //Enum + Tag string `json:"tag"` + Name string `json:"name"` + Code string `json:"code"` + RightsOwnedBy *Distributor `json:"rightsOwnedBy,omitempty"` + Next []*Place `json:"children,omitempty"` + up *Place +} + +func fmtPlace(place Place) string { + return fmt.Sprintf("%s (%s)", place.Name, place.Code) +} + +func (place Place) getType() int { + return int(place.Type) +} + +func (place *Place) setType(tier int) { + place.Type = Tier(tier) +} + +func (place Place) fmtPlaceWithRights() string { + return fmt.Sprintf("%s (%s) - %s", place, place.Id, place.RightsOwnedBy) +} + +func (place Place) String() string { + return fmtPlace(place) +} + +func (place *Place) AddChildNode(childNode *Place) { + place.addNext(childNode) + childNode.addUp(place) +} + +func (place *Place) AddParentNode(parentNode *Place) { + parentNode.addNext(place) + place.addUp(parentNode) +} + +func (place *Place) addUp(up *Place) { + place.up = up +} + +func (place *Place) addNext(next *Place) { + place.Next = append(place.Next, next) +} + +func placesToTags(places []*Place) []string { + strs := []string{} + for _, place := range places { + strs = append(strs, place.Tag) + } + return strs +} + +func (place *Place) isDistributorPresent() bool { + return place.RightsOwnedBy != nil +} + +func (place *Place) isDistributor(dist *Distributor) bool { + return place.RightsOwnedBy == dist +} + +func (place *Place) removeDistributor(parent *Place) { + if parent.RightsOwnedBy == nil { + place.removeDistributorInternal() + return + } + + for _, exclude := range parent.RightsOwnedBy.excludes { + if exclude == place { + place.removeDistributor(parent.up) + return + } + } + place.RightsOwnedBy = parent.RightsOwnedBy +} + +func (place *Place) removeDistributorInternal() { + place.RightsOwnedBy = nil +} + +//----------------------------------------------------------------------------- +// Dma - Methods and helper functions +//----------------------------------------------------------------------------- + +func (dma *Dma) GetPlaces() []*Place { + return dma.data.places.Next +} + +func (dma *Dma) GetDistributors() []*Distributor { + return dma.data.distributors.next +} + +func (dma *Dma) queryToPlace(query string) (*Place, error) { + dma.RLock() + defer dma.RUnlock() + place, ok := dma.lookup.places[query] + if !ok { + return nil, fmt.Errorf("queried place is not supported") + } + return place, nil +} + +func (dma *Dma) PrintDma(query string) { + dma.RLock() + defer dma.RUnlock() + + if place, ok := dma.lookup.places[query]; ok { + printDmaInternal(place) + fmt.Println("Place Id:", place.Id) + fmt.Println("Distributor:\n", place.RightsOwnedBy) + } else { + fmt.Println(query, "not found") + } + +} + +func printDmaInternal(place *Place) { + if place == nil { + return + } + printDmaInternal(place.up) + fmt.Printf("%s: %s\n", place.Type, place) +} + +// +// Function name: validateRow +// +// Purpose: Function to validate the CSV rows and record any descrepency +// Found out some duplicate values but choose to proceed without reporting as error +// Output of this function is a linked list for every row in the CSV that would be +// later used to update the dma's double linked list +// + +func validateRow(slice []string) (*Place, error) { + if slice != nil && len(slice) != HEIRARCHY*2 { + return nil, fmt.Errorf("there is discrepency in the data loaded from CSV") + } else { + + makeTag := func(index int, slice []string) string { + tag := slice[HEIRARCHY-1] + for i := HEIRARCHY - 2; i >= index; i-- { + tag = fmt.Sprintf("%s-%s", tag, slice[i]) + } + if len(tag) == 0 { + tag = slice[index] + } + return tag + } + + var leaf *Place + var root *Place + for i := 0; i < HEIRARCHY; i++ { + place := Place{} + place.Id = uuid.New() + place.Code = slice[i] + place.Name = slice[HEIRARCHY+i] + place.setType(i) + place.Tag = makeTag(i, slice) + if leaf == nil { + leaf = &place + root = leaf + } else { + root.addUp(&place) + root = &place + } + + } + + return leaf, nil + } + +} + +// +// Function name: populateData +// +// Purpose: Helper function used by the the generator function (InitDma) to populate the +// Dma's double linked list +// + +func populateData(dma *Dma, leaf *Place, logger *instrumentation.GoLogger) *Place { + + if leaf == nil { + return dma.data.places + } + + if present, ok := dma.lookup.places[leaf.Tag]; ok { + + return present + } + parent := populateData(dma, leaf.up, logger) + dma.Lock() + defer dma.Unlock() + parent.AddChildNode(leaf) + dma.lookup.places[leaf.Tag] = leaf + return leaf +} + +// +// Function name: InitDma +// +// Purpose: Generator function to initialized the Dma with the places double linked list +// which is the backbone of this datastructure +// + +func InitDma(config *config.Config, logger *instrumentation.GoLogger) (*Dma, error) { + csv, err := loadcsv.LoadCsv(config.Data.FilePath) + if err != nil { + logger.Error("Error in InitDma: %s", err) + } + + dma := Dma{} + dma.lookup.places = make(map[string]*Place) + dma.lookup.distributors = make(map[string]*Distributor) + world := Place{ + Id: uuid.New(), + Name: "World", + Code: "World", + } + dma.data.places = &world + dma.data.distributors = &Distributor{ + Id: uuid.New(), + } + + for i, row := range csv { + if i == 0 { + continue + //skipping first header + } + place, err := validateRow(row) + if err != nil { + logger.Error("%s", err) + return nil, err + } + populateData(&dma, place, logger) + + } + + dma.updatedTime = time.Now() + if dma.data.places == nil { + return nil, fmt.Errorf("the initialization failed") + } + return &dma, nil +} + +func (dma *Dma) PrintPlaces() { + dma.RLock() + defer dma.RUnlock() + + printPlacesInternal(dma.data.places, 3) +} + +func (dma *Dma) PrintPlacesLookup() { + dma.RLock() + defer dma.RUnlock() + for tag, place := range dma.lookup.places { + fmt.Println(tag, ":", place) + } +} + +func (dma *Dma) PrintPlacesFrom(query string) { + dma.RLock() + defer dma.RUnlock() + place, err := dma.queryToPlace(query) + if err != nil { + fmt.Println(err) + } + printPlacesInternal(place, place.getType()) +} + +func (dma *Dma) GetPlaceByTag(query string) (*Place, error) { + dma.RLock() + defer dma.RUnlock() + if result, ok := dma.lookup.places[query]; ok { + return result, nil + } + + return nil, fmt.Errorf("the place with tag '%s' is not found", query) +} + +func printPlacesInternal(node *Place, stage int) { + if node == nil { + return + } + fmt.Println(strings.Repeat("\t", HEIRARCHY-stage), Tier(stage), node.fmtPlaceWithRights()) + // fmt.Printf("%s\n", node.next) + for _, child := range node.Next { + printPlacesInternal(child, stage-1) + } +} + +// Distributor Datastructure + +// I am looking to have a tight coupling between DMA and Disbributor Datastructures +// This should help us retrieve information at a time complexity of O(1) + +type Distributor struct { + Id uuid.UUID `json:"id"` + Name string `json:"name"` + includes []*Place + excludes []*Place + up *Distributor + next []*Distributor +} + +func (dist Distributor) GetIncludesAsTags() []string { + return placesToTags(dist.includes) +} + +func (dist Distributor) GetExcludesAsTags() []string { + return placesToTags(dist.excludes) +} + +func (dist *Distributor) String() string { + if dist == nil { + return "No distributor" + } + return fmt.Sprintf("%s (%s)", dist.Name, dist.Id) +} + +func (dist Distributor) PrintDistributorDetails() string { + return fmt.Sprintf("%s: %s\n - Include: %q\n -Exclude %q\n", dist.Id, dist.Name, dist.includes, dist.excludes) +} + +func (dma *Dma) PrintDistributors() { + dma.RLock() + defer dma.RUnlock() + printDistrbibutorsInternal(dma.data.distributors, 0) +} + +func printDistrbibutorsInternal(node *Distributor, stage int) { + if node == nil { + return + } + + fmt.Println(stage, node.PrintDistributorDetails()) + + for _, child := range node.next { + printDistrbibutorsInternal(child, stage+1) + } + +} + +func (dma *Dma) GetDistributor(name string) (*Distributor, error) { + dma.RLock() + defer dma.RUnlock() + + if dist, ok := dma.lookup.distributors[name]; ok { + return dist, nil + } else { + return nil, fmt.Errorf("distributor not found in the list") + } +} + +func (dma *Dma) ProcessTagInRequest(tags []string) ([]*Place, error) { + places := []*Place{} + for _, tag := range tags { + place, err := dma.GetPlaceByTag(tag) + if err != nil { + return nil, err + } + places = append(places, place) + } + return places, nil +} + +func (dma *Dma) AddDistributor(name string, parent *Distributor) (*Distributor, error) { + dma.Lock() + defer dma.Unlock() + if existingDistributor, ok := dma.lookup.distributors[name]; ok { + return existingDistributor, fmt.Errorf("distributor already present in the list") + } + dist := &Distributor{ + Id: uuid.New(), + Name: name, + } + if parent == nil { + parent = dma.data.distributors + } + parent.next = append(parent.next, dist) + dist.up = parent + + //lookup by name + dma.lookup.distributors[name] = dist + //lookup by id + dma.lookup.distributors[dist.Id.String()] = dist + return dist, nil +} + +func (dma *Dma) DeleteDistributor(name string) error { + dma.Lock() + defer dma.Unlock() + if existingDistributor, ok := dma.lookup.distributors[name]; ok { + fetchedName := existingDistributor.Name + fetchedId := existingDistributor.Id.String() + parent := existingDistributor.up + for index, child := range parent.next { + if child == existingDistributor { + // delete distributor from linked list + parent.next = append(parent.next[0:index], parent.next[index+1:]...) + // delete distributor reference from lookup map + delete(dma.lookup.distributors, fetchedId) + delete(dma.lookup.distributors, fetchedName) + + // delete all reference in places + for _, place := range existingDistributor.includes { + excludeDistributor(dma.lookup.places[place.Tag], existingDistributor) + } + return nil + } + } + } + return fmt.Errorf("distributor %s not present", name) + +} + +func (dma *Dma) DeleteDistributorInclude(distributor *Distributor, deletePlacesStr []string, + logger instrumentation.GoLogger) error { + defer logger.TimeTheFunction(time.Now(), "DeleteDistributorInclude") + + deletePlaces := make(map[string]*Place) + for _, value := range deletePlacesStr { + place, err := dma.queryToPlace(value) + if err != nil { + return err + } + deletePlaces[place.Id.String()] = place + } + + updatedIncludes := []*Place{} + + dma.Lock() + defer dma.Unlock() + for _, include := range distributor.includes { + if place, ok := deletePlaces[include.Id.String()]; ok { + excludeDistributor(place, distributor) + continue + } + updatedIncludes = append(updatedIncludes, include) + } + distributor.includes = updatedIncludes + return nil +} + +func (dma *Dma) DeleteDistributorExclude(distributor *Distributor, deletePlacesStr []string, + logger instrumentation.GoLogger) error { + defer logger.TimeTheFunction(time.Now(), "DeleteDistributorExclude") + + deletePlaces := make(map[string]*Place) + for _, value := range deletePlacesStr { + place, err := dma.queryToPlace(value) + if err != nil { + return err + } + + deletePlaces[place.Id.String()] = place + } + + updatedExcludes := []*Place{} + + dma.Lock() + defer dma.Unlock() + for _, exclude := range distributor.excludes { + + if place, ok := deletePlaces[exclude.Id.String()]; ok { + assignDistributor(place, distributor) + continue + } + updatedExcludes = append(updatedExcludes, exclude) + } + + distributor.excludes = updatedExcludes + return nil +} + +func (dma *Dma) appendDistributorInclude(distributor *Distributor, place *Place, + logger instrumentation.GoLogger) error { + defer logger.TimeTheFunction(time.Now(), "appendDistributorInclude") + dma.Lock() + defer dma.Unlock() + + if place.isDistributorPresent() && !place.up.isDistributor(distributor.up) { + return nil + } else { + assignDistributor(place, distributor) + } + temp := dma.lookup.distributors[distributor.Name] + temp.includes = append(temp.includes, + place) + dma.lookup.distributors[distributor.Name] = temp + return nil +} + +func (dma *Dma) appendDistributorExclude(distributor *Distributor, place *Place, logger instrumentation.GoLogger) error { + defer logger.TimeTheFunction(time.Now(), "appendDistributorExclude") + dma.Lock() + defer dma.Unlock() + + temp := dma.lookup.distributors[distributor.Name] + temp.excludes = append(temp.excludes, + place) + dma.lookup.distributors[distributor.Name] = temp + + if place.isDistributor(distributor) { + excludeDistributor(place, distributor) + } else { + logger.Info(distributor.Name, distributorAlreadyExcludedError, place) + } + + return nil +} + +func (dma *Dma) IncludeDistributorPermission(distributor *Distributor, includes []string, excludes []string, + logger instrumentation.GoLogger) error { + + defer logger.TimeTheFunction(time.Now(), "IncludeDistributorPermission") + + err := dma.CheckConflictBeforeChange(distributor, includes, excludes, logger) + if err != nil { + logger.Error(err.Error()) + return err + } + + logger.Info("adding inclusions") + for _, include := range includes { + place, err := dma.queryToPlace(fmt.Sprint(include)) + if err != nil { + return err + } + err = dma.appendDistributorInclude(distributor, place, logger) + if err != nil { + return err + } + } + + logger.Info("adding exclusions") + for _, exclude := range excludes { + place, err := dma.queryToPlace(fmt.Sprint(exclude)) + if err != nil { + return err + } + err = dma.appendDistributorExclude(distributor, place, logger) + if err != nil { + return err + } + } + + return nil + +} + +func (dma *Dma) CheckConflictBeforeChange(distributor *Distributor, includes []string, excludes []string, + logger instrumentation.GoLogger) error { + + defer logger.TimeTheFunction(time.Now(), "CheckConflictBeforeChange") + + logger.Info("fetching inclusions") + inclusionPlaces := make(map[string]*Place) + for _, include := range includes { + place, err := dma.queryToPlace(include) + if err != nil { + return err + } + inclusionPlaces[place.Id.String()] = place + } + for _, place := range distributor.includes { + inclusionPlaces[place.Id.String()] = place + } + logger.Info(fmt.Sprintf("%q", inclusionPlaces)) + + logger.Info("fetching exclusions") + logger.Info(fmt.Sprintf("%q", excludes)) + + exclusionPlaces := make(map[string]*Place) + for _, exclude := range excludes { + place, err := dma.queryToPlace(exclude) + if err != nil { + return err + } + exclusionPlaces[place.Id.String()] = place + } + for _, place := range distributor.excludes { + exclusionPlaces[place.Id.String()] = place + } + + logger.Info(fmt.Sprintf("%q", exclusionPlaces)) + + err := CheckConflictDistributor(dma, distributor, inclusionPlaces, exclusionPlaces) + if err != nil { + + return err + } + return nil +} + +func assignDistributor(place *Place, dist *Distributor) { + if place == nil { + return + } + + place.RightsOwnedBy = dist + + for _, child := range place.Next { + assignDistributor(child, dist) + } +} + +func excludeDistributor(place *Place, dist *Distributor) { + if place == nil { + return + } + + if place.isDistributor(dist) { + place.removeDistributor(place.up) + } + + for _, child := range place.Next { + excludeDistributor(child, dist) + } +} + +func CheckConflictDistributor(dma *Dma, dist *Distributor, includes map[string]*Place, exlcudes map[string]*Place) error { + for _, child := range includes { + err := checkConflictDistributorIncludesSubsetOfExcludes(dma, child, dist, exlcudes) + if err != nil { + return err + } + err = checkConflictDistributorIncludes(dma, child, dist, exlcudes) + if err != nil { + return err + } + } + + for _, child := range exlcudes { + err := checkConflictDistributorExcludes(dma, child, dist, includes) + if err != nil { + return err + } + } + + return nil + +} + +func checkConflictDistributorIncludes(dma *Dma, node *Place, dist *Distributor, exlcudes map[string]*Place) error { + if node == nil { + return nil + } + //inclusion check + if node.RightsOwnedBy != dist.up && dist.up != dma.data.distributors { + + if _, ok := exlcudes[node.Id.String()]; ok { + return nil + } else { + return fmt.Errorf("parent(%s) lacks the rights to add the distributor", dist.up) + } + } + + for _, child := range node.Next { + err := checkConflictDistributorIncludes(dma, child, dist, exlcudes) + if err != nil { + return err + } + } + + return nil + +} + +func checkConflictDistributorIncludesSubsetOfExcludes(dma *Dma, node *Place, dist *Distributor, excludes map[string]*Place) error { + + for _, child := range excludes { + if result := checkConflictDistributorExcludesInternal(dma, child, dist, node); result { + return fmt.Errorf("include %s is part of the excludes of the distributor", node) + } + } + return nil + +} + +func checkConflictDistributorExcludes(dma *Dma, node *Place, dist *Distributor, includes map[string]*Place) error { + + for _, child := range includes { + if result := checkConflictDistributorExcludesInternal(dma, child, dist, node); !result { + return fmt.Errorf("exclude %s not part of the includes in the distributor", node) + } + } + return nil + +} + +func checkConflictDistributorExcludesInternal(dma *Dma, node *Place, dist *Distributor, exclude *Place) bool { + if node == nil { + return false + } + //exclusion check + if node == exclude { + return true + } + + for _, child := range node.Next { + if result := checkConflictDistributorExcludesInternal(dma, child, dist, exclude); result { + return result + } + } + return false + +} + +//----------------------------------------------------------------------------- +// QueryDma - Deprecated +//----------------------------------------------------------------------------- + +// Utility to query and print the DMA +type QueryDma struct { + CountryCode string `json:"cc"` + StateCode string `json:"stc,omitempty"` + CityCode string `json:"ctyc,omitempty"` +} + +func (query QueryDma) String() string { + output := "" + switch { + case query.CityCode == "" && query.StateCode == "" && query.CountryCode == "": + break + case query.CityCode == "" && query.StateCode == "": + output = query.CountryCode + case query.CityCode == "": + output = fmt.Sprintf("%s-%s", query.CountryCode, query.StateCode) + default: + output = fmt.Sprintf("%s-%s-%s", query.CountryCode, query.StateCode, query.CityCode) + } + return output +} + +func (dma *Dma) queryDmaToPlaces(queries []QueryDma) ([]*Place, error) { + places := []*Place{} + for _, query := range queries { + place, err := dma.queryToPlace(fmt.Sprint(query)) + if err != nil { + return []*Place{}, err + } + places = append(places, place) + + } + return places, nil +} diff --git a/dma/dma_test.go b/dma/dma_test.go new file mode 100644 index 000000000..07b08d50e --- /dev/null +++ b/dma/dma_test.go @@ -0,0 +1,10 @@ +package dma_test + +import "testing" + +func TestInitDma(t *testing.T){ + t.Run("Test Init Dma", func(t *testing.T) { + + }) +} + diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..085687596 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/ghanithan/challenge2016 + +go 1.23 + +require ( + github.com/google/uuid v1.6.0 + github.com/gorilla/handlers v1.5.2 + github.com/gorilla/mux v1.8.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require github.com/felixge/httpsnoop v1.0.3 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..2b7c07164 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/deletedistributor.go b/handlers/deletedistributor.go new file mode 100644 index 000000000..c7866ad23 --- /dev/null +++ b/handlers/deletedistributor.go @@ -0,0 +1,35 @@ +package handlers + +import ( + "fmt" + "net/http" + "strings" +) + +func (service *Service) DeleteDistributor() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + query := r.URL.Query().Get("id") + if len(query) == 0 { + query = r.URL.Query().Get("name") + } + + if len(query) != 0 { + values := strings.Split(query, ",") + for _, value := range values { + err := service.DmaService.DeleteDistributor(value) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusNotFound, fmt.Sprintf("distributor %s is not found", value)) + return + } + } + + } else { + FailureResponse(w, http.StatusUnprocessableEntity, "query parameter 'name' missing") + } + + SuccessResponse(w, nil) + + }) +} diff --git a/handlers/getdistributor copy.go b/handlers/getdistributor copy.go new file mode 100644 index 000000000..c7c9b52b3 --- /dev/null +++ b/handlers/getdistributor copy.go @@ -0,0 +1,65 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/ghanithan/challenge2016/dma" +) + +type GetDistributorResponse struct { + Id string `json:"id"` + Name string `json:"name"` + Includes []string `json:"includedPlaces"` + Excludes []string `json:"excludedPlaces"` +} + +func (service *Service) GetDistributor() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var response []byte + var err error + + // both query by id and query by name is supported + // Highest priority goes to id + query := r.URL.Query().Get("id") + if len(query) == 0 { + query = r.URL.Query().Get("name") + } + if len(query) != 0 { + dists := []GetDistributorResponse{} + values := strings.Split(query, ",") + for _, value := range values { + var dist *dma.Distributor + dist, err = service.DmaService.GetDistributor(value) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusNotFound, fmt.Sprintf("distributor %s is not found", value)) + return + } + distributor := GetDistributorResponse{ + Includes: dist.GetIncludesAsTags(), + Excludes: dist.GetExcludesAsTags(), + Id: string(dist.Id.String()), + Name: dist.Name, + } + dists = append(dists, distributor) + } + + response, err = json.Marshal(dists) + + } else { + dists := service.DmaService.GetDistributors() + response, err = json.Marshal(dists) + } + + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, "") + return + } + SuccessResponse(w, response) + + }) +} diff --git a/handlers/getdistributorpermission.go b/handlers/getdistributorpermission.go new file mode 100644 index 000000000..5d7493c61 --- /dev/null +++ b/handlers/getdistributorpermission.go @@ -0,0 +1,76 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/ghanithan/challenge2016/dma" + "github.com/gorilla/mux" +) + +type GetDistributorPermissionResponse struct { + Place Place `json:"place"` + Permitted bool `json:"permitted"` +} + +type Place struct { + Name string `json:"name"` + Id string `json:"id"` + Tag string `json:"tag"` +} + +func dmaPlacetoPlace(place *dma.Place) Place { + return Place{ + Name: place.String(), + Id: place.Id.String(), + Tag: place.Tag, + } +} + +func (service *Service) GetDistributorPermission() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + pathParam, ok := vars["distributor"] + if !ok { + FailureResponse(w, http.StatusBadRequest, "the distributor path paramter is not present. '/permission/{distirbutorId}?in=placeTag1,placeTag2") + return + } + + distributor, err := service.DmaService.GetDistributor(pathParam) + if err != nil { + FailureResponse(w, http.StatusNotFound, err.Error()) + return + } + response := []GetDistributorPermissionResponse{} + if queryIn := r.URL.Query().Get("in"); len(queryIn) != 0 { + // Adding support to filter multiple places in the same request + queries := strings.Split(queryIn, ",") + for _, query := range queries { + queryResult, err := service.DmaService.GetPlaceByTag(query) + if err != nil { + FailureResponse(w, http.StatusNotFound, err.Error()) + return + } + + responseValue := GetDistributorPermissionResponse{ + Place: dmaPlacetoPlace(queryResult), + Permitted: queryResult.RightsOwnedBy == distributor, + } + response = append(response, responseValue) + } + + } else { + FailureResponse(w, http.StatusBadRequest, "the distributor query paramter is not present. '/permission/{distirbutorId}?in=placeTag1,placeTag2") + return + } + + if dataJson, err := json.Marshal(response); err != nil { + service.Logger.Error("Failed to send premission list ", service.Logger.String("host", r.Host)) + FailureResponse(w, http.StatusNotFound, "Not Found") + } else { + SuccessResponse(w, dataJson) + } + + }) +} diff --git a/handlers/getlistplaces.go b/handlers/getlistplaces.go new file mode 100644 index 000000000..be802a3a5 --- /dev/null +++ b/handlers/getlistplaces.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/ghanithan/challenge2016/dma" +) + +func (service *Service) GetListPlaces() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + data := []*dma.Place{} + if queryIn := r.URL.Query().Get("in"); len(queryIn) != 0 { + // Adding support to filter multiple places in the same request + queries := strings.Split(queryIn, ",") + for _, query := range queries { + queryResult, err := service.DmaService.GetPlaceByTag(query) + if err != nil { + FailureResponse(w, http.StatusNotFound, err.Error()) + return + } + data = append(data, queryResult) + } + } else { + // List all places in the dma + data = service.DmaService.GetPlaces() + } + + if dataJson, err := json.Marshal(data); err != nil { + service.Logger.Error("Failed to send places list ", service.Logger.String("host", r.Host)) + FailureResponse(w, http.StatusNotFound, "Not Found") + } else { + SuccessResponse(w, dataJson) + } + + }) +} diff --git a/handlers/getversion.go b/handlers/getversion.go new file mode 100644 index 000000000..6415ce2bf --- /dev/null +++ b/handlers/getversion.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +type Version struct { + Version string `json:"version"` +} + +func (service *Service) GetVersion() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + version := Version{ + Version: service.Context.Value("version").(string), + } + + if versionJson, err := json.Marshal(version); err != nil { + service.Logger.Error("Failed to send version information", service.Logger.String("host", r.Host)) + FailureResponse(w, http.StatusNotFound, "Not Found") + } else { + SuccessResponse(w, versionJson) + } + + }) +} diff --git a/handlers/patchupdatedistributor.go b/handlers/patchupdatedistributor.go new file mode 100644 index 000000000..e4ab10459 --- /dev/null +++ b/handlers/patchupdatedistributor.go @@ -0,0 +1,85 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/ghanithan/challenge2016/dma" +) + +type UpdateDistributorRequest struct { + Name string `json:"name"` + Include Change `json:"include"` + Exclude Change `json:"exclude"` +} + +type Change struct { + Add []string `json:"add"` + Delete []string `json:"delete"` +} + +func (service *Service) PatchUpdateDistributor() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, "invalid body") + return + } + defer r.Body.Close() + + request := UpdateDistributorRequest{} + + err = json.Unmarshal(body, &request) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, "invalid json") + return + } + + var distributor *dma.Distributor + + distributor, err = service.DmaService.GetDistributor(request.Name) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusNotFound, fmt.Sprintf("distributor '%s' not found", request.Name)) + return + } + + err = service.DmaService.IncludeDistributorPermission(distributor, request.Include.Add, + request.Exclude.Add, *service.Logger) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, + fmt.Sprintf("could not update the distributor with the inclusions: %s", err)) + return + } + + err = service.DmaService.DeleteDistributorInclude(distributor, request.Include.Delete, *service.Logger) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, + fmt.Sprintf("could not update the distributor with deletions: %s", err)) + return + } + + err = service.DmaService.DeleteDistributorExclude(distributor, request.Exclude.Delete, *service.Logger) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, + fmt.Sprintf("could not update the distributor with deletions: %s", err)) + return + } + + jsonDist, err := json.Marshal(distributor) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, "") + return + } + + SuccessResponse(w, jsonDist) + }) +} diff --git a/handlers/postaddnewdistributor.go b/handlers/postaddnewdistributor.go new file mode 100644 index 000000000..c49d9ffb3 --- /dev/null +++ b/handlers/postaddnewdistributor.go @@ -0,0 +1,75 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/ghanithan/challenge2016/dma" +) + +type AddNewDistributorRequest struct { + Name string `json:"name"` + Include []string `json:"include"` + Exclude []string `json:"exclude"` + Parent string `json:"parent"` +} + +func (service *Service) PostAddNewDistributor() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, "invalid body") + return + } + defer r.Body.Close() + + request := AddNewDistributorRequest{} + + err = json.Unmarshal(body, &request) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, "invalid json") + return + } + + var parent *dma.Distributor + if len(request.Parent) != 0 { + parent, err = service.DmaService.GetDistributor(request.Parent) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusNotFound, fmt.Sprintf("parent '%s' not found", request.Parent)) + return + } + } + fmt.Printf("here %q\n", parent) + dist, err := service.DmaService.AddDistributor(request.Name, parent) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, "") + return + } + + err = service.DmaService.IncludeDistributorPermission(dist, request.Include, request.Exclude, *service.Logger) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, + fmt.Sprintf("could not add the distributor: %s", err)) + if err = service.DmaService.DeleteDistributor(request.Name); err != nil { + service.Logger.Error(err.Error()) + } + return + } + + jsonDist, err := json.Marshal(dist) + if err != nil { + service.Logger.Error(err.Error()) + FailureResponse(w, http.StatusInternalServerError, "") + return + } + + SuccessResponse(w, jsonDist) + }) +} diff --git a/handlers/response.go b/handlers/response.go new file mode 100644 index 000000000..73a498c28 --- /dev/null +++ b/handlers/response.go @@ -0,0 +1,18 @@ +package handlers + +import "net/http" + +func SuccessResponse(response http.ResponseWriter, serialized []byte) { + if len(serialized) == 0 { + response.WriteHeader(http.StatusNoContent) + return + } + response.Header().Add("Content-Type", "application/json") + response.WriteHeader(http.StatusOK) + response.Write(serialized) +} + +func FailureResponse(response http.ResponseWriter, status int, erroMessage string) { + response.WriteHeader(status) + response.Write([]byte(erroMessage)) +} diff --git a/handlers/service.go b/handlers/service.go new file mode 100644 index 000000000..d9af77617 --- /dev/null +++ b/handlers/service.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "context" + + "github.com/ghanithan/challenge2016/dma" + "github.com/ghanithan/challenge2016/instrumentation" + "github.com/gorilla/mux" +) + +type Service struct { + Context context.Context + DmaService *dma.Dma + Logger *instrumentation.GoLogger +} + +func (app *Service) AddHanlders(router *mux.Router) *mux.Router { + router.Handle("/version", app.GetVersion()).Methods("GET") + router.Handle("/list/places", app.GetListPlaces()).Methods("GET") + router.Handle("/distributor", app.PostAddNewDistributor()).Methods("POST") + router.Handle("/distributor", app.GetDistributor()).Methods("GET") + router.Handle("/distributor", app.DeleteDistributor()).Methods("DELETE") + router.Handle("/distributor", app.PatchUpdateDistributor()).Methods("PATCH") + router.Handle("/permission/{distributor}", app.GetDistributorPermission()).Methods("GET") + + return router +} diff --git a/instrumentation/instrumentation.go b/instrumentation/instrumentation.go new file mode 100644 index 000000000..5ece64a43 --- /dev/null +++ b/instrumentation/instrumentation.go @@ -0,0 +1,12 @@ +package instrumentation + +import "time" + +type Instrumentation interface { + Debug(msg string, args map[string]interface{}) + Info(msg string, args map[string]interface{}) + Warn(msg string, args map[string]interface{}) + Error(msg string, args map[string]interface{}) + TimeTheFunction(start time.Time, functionName string) + String(key string, value string) +} diff --git a/instrumentation/logger.go b/instrumentation/logger.go new file mode 100644 index 000000000..82407ca65 --- /dev/null +++ b/instrumentation/logger.go @@ -0,0 +1,42 @@ +package instrumentation + +import ( + "log" + "log/slog" + "os" + "time" +) + +type GoLogger struct { + logger *slog.Logger +} + +func InitInstruments() GoLogger { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + return GoLogger{logger} +} + +func (l *GoLogger) Debug(msg string, args ...any) { + l.logger.Debug(msg, args...) +} + +func (l *GoLogger) Info(msg string, args ...any) { + l.logger.Info(msg, args...) +} + +func (l *GoLogger) Warn(msg string, args ...any) { + l.logger.Warn(msg, args...) +} + +func (l *GoLogger) Error(msg string, args ...any) { + l.logger.Error(msg, args...) +} + +func (l *GoLogger) String(key string, value string) slog.Attr { + return slog.String(key, value) +} + +func (l *GoLogger) TimeTheFunction(start time.Time, functionName string) { + elapsed := time.Since(start) + log.Printf("%s took %dms to complete", functionName, elapsed.Nanoseconds()/1000) +} diff --git a/loadcsv/loadcsv.go b/loadcsv/loadcsv.go new file mode 100644 index 000000000..cf310ca54 --- /dev/null +++ b/loadcsv/loadcsv.go @@ -0,0 +1,25 @@ +package loadcsv + +import ( + "encoding/csv" + "fmt" + "os" +) + +func LoadCsv(filePath string) ([][]string, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("error in opening file: %s", err) + } + + defer file.Close() + + reader := csv.NewReader(file) + + csv, err := reader.ReadAll() + if err != nil { + return nil, fmt.Errorf("error in parsing the CSV file: %s", err) + } + //fmt.Println(csv) + return csv, nil +} diff --git a/loadcsv/loadcsv_test.go b/loadcsv/loadcsv_test.go new file mode 100644 index 000000000..ac413b6b5 --- /dev/null +++ b/loadcsv/loadcsv_test.go @@ -0,0 +1,14 @@ +package loadcsv + +import "testing" + +// Test using the comman `go test ./... -v` +func TestLoadCsv(t *testing.T) { + t.Run("Test loading and parsing CSV", func(t *testing.T) { + csv, err := LoadCsv("../cities.csv") + if err != nil { + t.Errorf("error occured in testing: %s", err) + } + t.Log(csv) + }) +} diff --git a/main.go b/main.go new file mode 100644 index 000000000..f348948e0 --- /dev/null +++ b/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/ghanithan/challenge2016/config" + "github.com/ghanithan/challenge2016/dma" + "github.com/ghanithan/challenge2016/instrumentation" + server "github.com/ghanithan/challenge2016/server" +) + +func main() { + fmt.Println("Starting Challenge 2016...") + + logger := instrumentation.InitInstruments() + + config, err := config.GetConfig(logger, "./setting/default.yaml") + + if err != nil { + logger.Error(err.Error()) + } + + qubeDma, err := dma.InitDma(config, &logger) + if err != nil { + logger.Error(err.Error()) + } + + // distributor, err := qubeDma.AddDistributor("distributor1", nil) + // if err != nil { + // logger.Error("%s", err) + // } + + // include := []string{"IN"} + // exclude := []string{} + + // qubeDma.IncludeDistributorPermission(distributor, include, exclude, logger) + + // qubeDma.PrintPlacesFrom("IN-TN-CENAI") + + service, cancel := server.InitServer(config, qubeDma, &logger) + defer cancel() + + // Start the server + go func() { + logger.Info("Server listening on ", logger.String("addr", service.HttpService.Addr)) + if err := service.HttpService.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed: %v", err) + } + }() + + // Graceful shutdown + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + <-stop + log.Println("Shutting down server...") + cancel() + +} diff --git a/openapispec_v1.yaml b/openapispec_v1.yaml new file mode 100644 index 000000000..00ce11076 --- /dev/null +++ b/openapispec_v1.yaml @@ -0,0 +1,195 @@ +openapi: 3.0.3 +info: + title: challenge2016 + version: 1.0.0 + contact: {} +servers: + - url: 'http://localhost:3000' + - url: 'https://challenge2016.onrender.com/' +paths: + /version: + get: + summary: version + description: version + operationId: version + responses: + '200': + description: '' + /list/places: + get: + summary: list places + description: list places + operationId: listPlaces + parameters: + - name: in + in: query + schema: + type: string + example: IN-TN,BE,US-VA + responses: + '200': + description: '' + /permission/{distributorId}: + get: + summary: Get permission + description: Get permission + operationId: getPermission + parameters: + - name: in + in: query + schema: + type: string + example: IN-TN,BE,US-VA + responses: + '200': + description: '' + parameters: + - name: distributorId + in: path + required: true + schema: + type: string + /distributor: + post: + summary: Add Distributor + description: Add Distributor + operationId: addDistributor + requestBody: + content: + application/json: + schema: + type: object + properties: + exclude: + type: array + items: + type: string + example: IN-TN-CENAI + example: + - IN-TN-CENAI + include: + type: array + items: + type: string + example: IN-TN + example: + - IN-TN + name: + type: string + example: distributor1 + examples: + Add Top-level Distributor: + value: + exclude: + - IN-TN-CENAI + include: + - IN-TN + name: distributor1 + Add Next-level Distributor: + value: + parent: distributor1 + include: + - IN-TN-COBTE + name: distributor2 + Add Next-level Distributor -ve Testcase: + value: + parent: distributor1 + include: + - IN-TN-CENAI + name: distributor3 + + responses: + '200': + description: '' + get: + summary: Get Distributor + description: Get Distributor + operationId: getDistributor + parameters: + - name: id + in: query + schema: + type: string + example: 5dae4cbc-b84f-44d4-ad0d-56fda7191cb4 + - name: name + in: query + schema: + type: string + example: distributor1 + responses: + '200': + description: '' + delete: + summary: Delete Distributor + description: Delete Distributor + operationId: deleteDistributor + parameters: + - name: name + in: query + schema: + type: string + example: distributor1 + responses: + '200': + description: '' + patch: + summary: Update Distributor + description: Update Distributor + operationId: updateDistributor + requestBody: + content: + application/json: + schema: + type: object + properties: + exclude: + type: object + properties: + add: + type: array + items: + type: string + example: IN-TN-COBTE + example: + - IN-TN-COBTE + delete: + type: array + items: {} + example: [] + include: + type: object + properties: + add: + type: array + items: + type: string + example: IN-TN-COBTE + example: + - IN-TN-COBTE + delete: + type: array + items: + type: string + example: IN-TN-CENAI + example: + - IN-TN-CENAI + name: + type: string + example: distributor1 + examples: + Update Distributor: + value: + exclude: + add: + - IN-TN-COBTE + delete: [] + include: + add: + - IN-TN-COBTE + delete: + - IN-TN-CENAI + name: distributor1 + responses: + '200': + description: '' +tags: [] diff --git a/render.yaml b/render.yaml new file mode 100644 index 000000000..fc212f96f --- /dev/null +++ b/render.yaml @@ -0,0 +1,29 @@ +################################################################# +# Example render.yaml # +# Do not use this file directly! Consult it for reference only. # +################################################################# + +# previews: +# generation: automatic # Enable preview environments + + # List all services *except* PostgreSQL databases here +services: + # A web service on the Ruby native runtime + - type: web + runtime: go + name: challenge2016 + repo: https://github.com/ghanithan/challenge2016 # Default: Repo containing render.yaml + numInstances: 1 # Manual scaling configuration. Default: 1 for new services + region: frankfurt # Default: oregon + plan: free # Default: starter + branch: solution # Default: master + buildCommand: go build -tags netgo -ldflags '-s -w' -o app + startCommand: ./app + autoDeploy: true # Disable automatic deploys + # maxShutdownDelaySeconds: 120 # Increase graceful shutdown period. Default: 30, Max: 300 + domains: # Custom domains + - ghanithan.com + - www.ghanithan.com + # envVars: # Environment variables + # key: API_BASE_URL + # value: https://api.ghanithan.com # Hardcoded value \ No newline at end of file diff --git a/server/service.go b/server/service.go new file mode 100644 index 000000000..c1ee4b089 --- /dev/null +++ b/server/service.go @@ -0,0 +1,64 @@ +package server + +import ( + "context" + "fmt" + "net/http" + + "github.com/ghanithan/challenge2016/config" + "github.com/ghanithan/challenge2016/dma" + "github.com/ghanithan/challenge2016/handlers" + "github.com/ghanithan/challenge2016/instrumentation" + ghandlers "github.com/gorilla/handlers" + "github.com/gorilla/mux" +) + +type Server struct { + HttpService *http.Server + AppService *handlers.Service +} + +func InitServer(config *config.Config, dmaService *dma.Dma, logger *instrumentation.GoLogger) (Server, context.CancelFunc) { + c := context.WithValue(context.Background(), "version", "0.0.1") + c, cancel := context.WithCancel(c) + router := mux.NewRouter() + + appService := handlers.Service{ + Context: c, + DmaService: dmaService, + Logger: logger, + } + // Catch all OPTIONS requests and handle CORS preflight + router.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PATCH, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept-Encoding") + w.WriteHeader(http.StatusOK) + }) + router.Use(corsMiddleware) + router.Use(ghandlers.CompressHandler) + + router = appService.AddHanlders(router) + + httpService := http.Server{ + Handler: router, + Addr: fmt.Sprintf("%s:%s", config.HttpServer.Host, config.HttpServer.Port), + } + + return Server{ + HttpService: &httpService, + AppService: &appService, + }, cancel +} + +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PATCH, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept-Encoding") + + // Proceed with the request + next.ServeHTTP(w, r) + }) +} diff --git a/setting/default.yaml b/setting/default.yaml new file mode 100644 index 000000000..b96062b8c --- /dev/null +++ b/setting/default.yaml @@ -0,0 +1,5 @@ +data: + filepath: ./cities.csv +httpserver: + host: 0.0.0.0 + port: 3000 \ No newline at end of file diff --git a/setting/sample.yaml b/setting/sample.yaml new file mode 100644 index 000000000..3b2368e32 --- /dev/null +++ b/setting/sample.yaml @@ -0,0 +1,4 @@ +data: + filepath: ../cities.csv +httpserver: + port: 3000 \ No newline at end of file