diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md new file mode 100644 index 000000000..593f19df0 --- /dev/null +++ b/INSTRUCTIONS.md @@ -0,0 +1,50 @@ +# Instructions + +```code +go mod tidy +go build -o bin/qube ./src && ./bin/qube +``` + +The application is a REST server and runs on Port 8000 + +### Endpoint +``` +GET /execute?command={commandString} +``` + +#### Commands +- Every command is a binary operation. +- The left operand is always a Distributor. +- The right operand can either be a Distributor or a Region. +- The format : `..` + +#### Operands +- A Distributor must be D +- A Region must be `--` +- The codes shall adhere to those used in cities.csv + +#### Operators +- Include : + + - Applicable to the both types of the right operand + - When the right is Distributor : D1.+.D2, it means D1 extends distributorship to D2 + - When the right is Region : D1.+.IN-TN, it means D1 can distribute in IN-TN + +- Exclude : - + - Applicable to the both types of the right operand + - When the right is Distributor : D1.-.D2, it means unless a Region is added in the future, D2 can't distribute in D1's Regions + - When the right is Region : D1.-.IN-TN, it means D2 cannot distribute in IN-TN + - D1.-.TN-IN means D1 cannot distribute in TN-IN + +- Print : @ + - The left operand must be a Distributor + - The right operand must be a Region + - The endpoint will answer the question of whether the distributor can operate in that region by "YES" or "NO" + +#### Examples for Curl +``` +curl "http://localhost:8000/execute?command=D1.%2B.TN-IN" +curl "http://localhost:8000/execute?command=D1.-.TN-IN" +curl "http://localhost:8000/execute?command=D1.@.TN-IN" +``` + +The command can be URL Encoded and hence, + is %2B diff --git a/bin/qube b/bin/qube new file mode 100644 index 000000000..16eeb4bb4 Binary files /dev/null and b/bin/qube differ diff --git a/commands.txt b/commands.txt new file mode 100644 index 000000000..76d676fdd --- /dev/null +++ b/commands.txt @@ -0,0 +1,7 @@ +D1.+.IN +D1.+.D2 +D2.+.IN-TN-KNGLM +D2.+.IN-TG +D2.+.D3 +D3.+.IN-TG +D3.-.IN-TG-HYDER \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..136f93007 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module qube-challenge-16 + +go 1.24.0 diff --git a/src/commands/commands.go b/src/commands/commands.go new file mode 100644 index 000000000..ff86001bd --- /dev/null +++ b/src/commands/commands.go @@ -0,0 +1,44 @@ +package commands + +import ( + "bufio" + "log" + "os" + "qube-challenge-16/src/dist" + "qube-challenge-16/src/geo" + "sync" +) + +type CommandExecutor struct { + GeoMap map[string]*geo.GeoNode + DistMap dist.DistMap + mu *sync.RWMutex +} + +func GenerateCommandExecutor() *CommandExecutor { + return &CommandExecutor{ + GeoMap: geo.GenerateGeoMap(), + DistMap: *dist.GenerateDistMap(), + mu: &sync.RWMutex{}, + } +} + +func ReadCommandsFromFile(fileName string) (commands []Command) { + file, err := os.Open(fileName) + if err != nil { + log.Fatalln("Error opening file:", err) + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + if command, err := GenerateCommand(scanner.Text()); err == nil { + commands = append(commands, command) + } else { + break + } + } + + return commands +} diff --git a/src/commands/commands_controllers.go b/src/commands/commands_controllers.go new file mode 100644 index 000000000..c30173a14 --- /dev/null +++ b/src/commands/commands_controllers.go @@ -0,0 +1,85 @@ +package commands + +import ( + "fmt" + "log" +) + +func (ce *CommandExecutor) ExecuteFromFile() { + pre_commands := ReadCommandsFromFile("commands.txt") + for _, command := range pre_commands { + log.Println("Preset command executed : ", ce.Execute(command)) + } +} + +func (ce *CommandExecutor) Execute(command Command) string { + switch command.Where { + case Geo: + if _, exists := ce.DistMap.Map[command.LeftOperand]; !exists { + ce.mu.Lock() + ce.DistMap.AddDistNode(command.LeftOperand) + ce.mu.Unlock() + } + switch command.Operation { + case Include: + parentDists := []string{} + + ce.mu.RLock() + for parentDist := range ce.DistMap.Map[command.LeftOperand].ParentIds { + parentDists = append(parentDists, parentDist) + } + ce.mu.RUnlock() + + ce.mu.Lock() + ce.GeoMap[command.RightOperand].IncludeDist(command.LeftOperand, parentDists) + ce.mu.Unlock() + + return fmt.Sprintf("%s included for %s", command.LeftOperand, command.RightOperand) + + case Exclude: + ce.mu.Lock() + ce.GeoMap[command.RightOperand].ExcludeDist(command.LeftOperand) + for _, childrenDist := range ce.DistMap.ChildrenMap[command.LeftOperand] { + ce.GeoMap[command.RightOperand].ExcludeDist(childrenDist) + } + ce.mu.Unlock() + + return fmt.Sprintf("%s excluded for %s", command.LeftOperand, command.RightOperand) + + default: + flag := "NO" + ce.mu.RLock() + parentDists := ce.DistMap.Map[command.LeftOperand].ParentIds + for dist := range ce.GeoMap[command.RightOperand].List { + if dist == command.LeftOperand { + for parentDist := range parentDists { + if _, exists := ce.GeoMap[command.RightOperand].List[parentDist]; !exists { + ce.GeoMap[command.RightOperand].ExcludeDist(command.LeftOperand) + break + } + flag = "YES" + } + break + } + } + ce.mu.RUnlock() + return flag + } + default: + switch command.Operation { + case Include: + ce.mu.Lock() + ce.DistMap.AddDistNodes(command.LeftOperand, command.RightOperand) + ce.mu.Unlock() + + return fmt.Sprintf("%s included to %s", command.RightOperand, command.LeftOperand) + case Exclude: + ce.mu.Lock() + ce.DistMap.RemoveChildDist(command.LeftOperand, command.RightOperand) + ce.mu.Unlock() + + return fmt.Sprintf("%s excluded from %s", command.RightOperand, command.LeftOperand) + } + } + return "" +} diff --git a/src/commands/commands_utils.go b/src/commands/commands_utils.go new file mode 100644 index 000000000..45ad28257 --- /dev/null +++ b/src/commands/commands_utils.go @@ -0,0 +1,83 @@ +package commands + +import ( + "errors" + "regexp" + "strings" +) + +const DistributorIDPattern = `^D\d+$` +const GeoPattern = `^([A-Z]{0,6})(-[A-Z]{0,6}){0,2}$` + +type OperationMode string + +const ( + Include OperationMode = "+" + Exclude OperationMode = "-" + Print OperationMode = "@" +) + +type WhichTree string + +const ( + Geo WhichTree = "Geo" + Dist WhichTree = "Dist" +) + +type Command struct { + LeftOperand string + Operation OperationMode + RightOperand string + Where WhichTree +} + +func validateDist(operand string) error { + DistributorRegex := regexp.MustCompile(DistributorIDPattern) + if valid := DistributorRegex.MatchString(operand); !valid { + return errors.New("invalid distributor") + } + return nil +} + +func validateGeo(operand string) error { + GeoRegex := regexp.MustCompile(GeoPattern) + if valid := GeoRegex.MatchString(operand); !valid { + return errors.New("invalid geo") + } + return nil +} + +func GenerateCommand(commandString string) (Command, error) { + parts := strings.Split(commandString, ".") + + var command Command + + if err := validateDist(parts[0]); err == nil { + command.LeftOperand = parts[0] + } else { + return Command{}, err + } + + switch parts[1] { + case "+": + command.Operation = Include + case "-": + command.Operation = Exclude + case "@": + command.Operation = Print + default: + return Command{}, errors.New("invalid operation") + } + + if err := validateDist(parts[2]); err == nil { + command.Where = Dist + command.RightOperand = parts[2] + } else if err := validateGeo(parts[2]); err == nil { + command.Where = Geo + command.RightOperand = parts[2] + } else { + return Command{}, errors.New("invalid dist/geo") + } + + return command, nil +} diff --git a/src/csv/csv.go b/src/csv/csv.go new file mode 100644 index 000000000..28964e4b9 --- /dev/null +++ b/src/csv/csv.go @@ -0,0 +1,40 @@ +package csv + +import ( + "encoding/csv" + "log" + "os" +) + +type GeoCSVRecord struct { + Country string + Province string + City string +} + +func ReadFromCSV(filename string) [][]string { + file, err := os.Open(filename) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if err != nil { + log.Fatal(err) + } + return records +} + +func ReadGeoFromCSV() (resp []GeoCSVRecord) { + records := ReadFromCSV("cities.csv") + for _, record := range records[1:] { + resp = append(resp, GeoCSVRecord{ + City: record[0], + Province: record[1], + Country: record[2], + }) + } + return resp +} diff --git a/src/dist/dist.go b/src/dist/dist.go new file mode 100644 index 000000000..f271bd7ed --- /dev/null +++ b/src/dist/dist.go @@ -0,0 +1,32 @@ +package dist + +import ( + "qube-challenge-16/src/tree" +) + +type DistNode struct { + tree.TreeNode + Id string + ParentIds map[string]bool +} + +func createDistNode(id string, parentId string) (dn *DistNode) { + dn = &DistNode{ + Id: id, + ParentIds: make(map[string]bool), + TreeNode: tree.TreeNode{ + Children: make(map[string]tree.Node), + }, + } + dn.ParentIds[parentId] = true + return dn +} + +func (d *DistNode) Traverse(operation func(tree.Node)) { + operation(d) + d.TreeNode.Traverse(operation) +} + +func (d *DistNode) RemoveBranch(childId string) { + delete(d.Children, childId) +} diff --git a/src/dist/dist_operations.go b/src/dist/dist_operations.go new file mode 100644 index 000000000..e58bbb256 --- /dev/null +++ b/src/dist/dist_operations.go @@ -0,0 +1,12 @@ +package dist + +import "qube-challenge-16/src/tree" + +func (d *DistNode) ListChildren() []string { + ChildrenList := []string{} + listOperation := func(di tree.Node) { + ChildrenList = append(ChildrenList, di.(*DistNode).Id) + } + d.Traverse(listOperation) + return ChildrenList +} diff --git a/src/dist/distmap.go b/src/dist/distmap.go new file mode 100644 index 000000000..f5d1faabc --- /dev/null +++ b/src/dist/distmap.go @@ -0,0 +1,48 @@ +package dist + +type DistMap struct { + Map map[string]*DistNode + ChildrenMap map[string][]string +} + +func GenerateDistMap() *DistMap { + return &DistMap{ + Map: make(map[string]*DistNode), + ChildrenMap: make(map[string][]string), + } +} + +func (dm *DistMap) AddDistNode(dist1 string) { + distNode := createDistNode(dist1, "") + dm.Map[dist1] = distNode +} + +func (dm *DistMap) AddDistNodes(dist1 string, dist2 string) { + var d1, d2 *DistNode + if _, exists := dm.Map[dist1]; !exists { + d1 = createDistNode(dist1, "") + dm.Map[dist1] = d1 + } else { + d1 = dm.Map[dist1] + } + if _, exists := dm.Map[dist2]; !exists { + d2 = createDistNode(dist2, dist1) + dm.Map[dist2] = d2 + } else { + d2 = dm.Map[dist2] + } + d1.AddBranch(d2.Id, d2) + d2.ParentIds[dist1] = true + dm.GenerateChildrenMap(dist1) +} + +func (dm *DistMap) RemoveChildDist(dist1 string, dist2 string) { + dm.Map[dist1].RemoveBranch(dist2) + dm.GenerateChildrenMap(dist1) + + delete(dm.Map[dist2].ParentIds, dist1) +} + +func (dm *DistMap) GenerateChildrenMap(dist string) { + dm.ChildrenMap[dist] = dm.Map[dist].ListChildren() +} diff --git a/src/geo/geo.go b/src/geo/geo.go new file mode 100644 index 000000000..661752d41 --- /dev/null +++ b/src/geo/geo.go @@ -0,0 +1,36 @@ +package geo + +import ( + "qube-challenge-16/src/tree" +) + +type Geography string + +const ( + Country Geography = "Country" + Province Geography = "Province" + City Geography = "City" +) + +type GeoNode struct { + tree.TreeNode + Id string + Type Geography + List map[string]bool +} + +func createGeoNode(entityId string, entityType Geography) *GeoNode { + return &GeoNode{ + TreeNode: tree.TreeNode{ + Children: make(map[string]tree.Node), + }, + Id: entityId, + Type: entityType, + List: make(map[string]bool), + } +} + +func (g *GeoNode) Traverse(operation func(tree.Node)) { + operation(g) + g.TreeNode.Traverse(operation) +} diff --git a/src/geo/geo_operations.go b/src/geo/geo_operations.go new file mode 100644 index 000000000..83704337a --- /dev/null +++ b/src/geo/geo_operations.go @@ -0,0 +1,36 @@ +package geo + +import ( + "qube-challenge-16/src/tree" +) + +func (g *GeoNode) IncludeDist(dist string, parentDists []string) { + includeOperation := func(g tree.Node) { + flag := true + for _, parentDist := range parentDists { + if _, exists := g.(*GeoNode).List[parentDist]; !exists && parentDist != "" { + flag = false + } + } + if flag { + g.(*GeoNode).List[dist] = true + } else { + delete(g.(*GeoNode).List, dist) + } + } + g.Traverse(includeOperation) +} + +func (g *GeoNode) ExcludeDist(dist string) { + excludeOperation := func(g tree.Node) { delete(g.(*GeoNode).List, dist) } + g.Traverse(excludeOperation) +} + +func (g *GeoNode) ConditionalExcludeDist(dist string, parentDist string) { + conditionalExcludeOperation := func(g tree.Node) { + if _, exists := g.(*GeoNode).List[parentDist]; exists { + delete(g.(*GeoNode).List, dist) + } + } + g.Traverse(conditionalExcludeOperation) +} diff --git a/src/geo/geomap.go b/src/geo/geomap.go new file mode 100644 index 000000000..38214f172 --- /dev/null +++ b/src/geo/geomap.go @@ -0,0 +1,51 @@ +package geo + +import ( + "qube-challenge-16/src/csv" +) + +func (g *GeoNode) addEdge(child *GeoNode) { + switch g.Type { + case Country: + g.AddBranch(child.Id, child) + case Province: + g.AddBranch(child.Id, child) + } +} + +func GenerateGeoMap() map[string]*GeoNode { + GeoMap := make(map[string]*GeoNode) + + key := func(geo Geography, geoRecord csv.GeoCSVRecord) string { + switch geo { + case "Country": + return geoRecord.Country + case "Province": + return geoRecord.Country + "-" + geoRecord.Province + case "City": + return geoRecord.Country + "-" + geoRecord.Province + "-" + geoRecord.City + } + return "" + } + + geoRecords := csv.ReadGeoFromCSV() + for _, geoRecord := range geoRecords { + countryKey := key(Country, geoRecord) + provinceKey := key(Province, geoRecord) + cityKey := key(City, geoRecord) + + if _, exists := GeoMap[countryKey]; !exists { + GeoMap[countryKey] = createGeoNode(geoRecord.Country, Country) + } + if _, exists := GeoMap[provinceKey]; !exists { + GeoMap[provinceKey] = createGeoNode(geoRecord.Province, Province) + GeoMap[countryKey].addEdge(GeoMap[provinceKey]) + } + if _, exists := GeoMap[cityKey]; !exists { + GeoMap[cityKey] = createGeoNode(geoRecord.City, City) + GeoMap[provinceKey].addEdge(GeoMap[cityKey]) + } + } + + return GeoMap +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 000000000..70b43ba2a --- /dev/null +++ b/src/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "log" + "net/http" + "qube-challenge-16/src/commands" +) + +func main() { + commandExec := commands.GenerateCommandExecutor() + commandExec.ExecuteFromFile() + + http.HandleFunc("/execute", func(w http.ResponseWriter, r *http.Request) { + queryParams := r.URL.Query() + + commandString := queryParams.Get("command") + log.Println("Received command : ", commandString) + command, error := commands.GenerateCommand(commandString) + if error != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(error.Error())) + } + + output := commandExec.Execute(command) + w.WriteHeader(http.StatusOK) + w.Write([]byte(output)) + }) + + http.ListenAndServe(":8000", nil) +} diff --git a/src/tree/tree.go b/src/tree/tree.go new file mode 100644 index 000000000..19d90342f --- /dev/null +++ b/src/tree/tree.go @@ -0,0 +1,20 @@ +package tree + +type Node interface { + AddBranch(childId string, child Node) + Traverse(operation func(Node)) +} + +type TreeNode struct { + Children map[string]Node +} + +func (t *TreeNode) AddBranch(childId string, child Node) { + t.Children[childId] = child +} + +func (t *TreeNode) Traverse(operation func(Node)) { + for _, child := range t.Children { + child.Traverse(operation) + } +}