diff --git a/README.md b/README.md index f1c342f65..0d6414603 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,54 @@ # Real Image Challenge 2016 -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: -``` -Permissions for DISTRIBUTOR1 -INCLUDE: INDIA -INCLUDE: UNITEDSTATES -EXCLUDE: KARNATAKA-INDIA -EXCLUDE: CHENNAI-TAMILNADU-INDIA -``` -This allows `DISTRIBUTOR1` to distribute in any city inside the United States and India, *except* cities in the state of Karnataka (in India) and the city of Chennai (in Tamil Nadu, India). - -At this point, asking your program if `DISTRIBUTOR1` has permission to distribute in `CHICAGO-ILLINOIS-UNITEDSTATES` should get `YES` as the answer, and asking if distribution can happen in `CHENNAI-TAMILNADU-INDIA` should of course be `NO`. Asking if distribution is possible in `BANGALORE-KARNATAKA-INDIA` should also be `NO`, because the whole state of Karnataka has been excluded. - -Sometimes, a distributor might split the work of distribution amount smaller sub-distiributors inside their authorized geographies. For instance, `DISTRIBUTOR1` might assign the following permissions to `DISTRIBUTOR2`: - -``` -Permissions for DISTRIBUTOR2 < DISTRIBUTOR1 -INCLUDE: INDIA -EXCLUDE: TAMILNADU-INDIA -``` -Now, `DISTRIBUTOR2` can distribute the movie anywhere in `INDIA`, except inside `TAMILNADU-INDIA` and `KARNATAKA-INDIA` - `DISTRIBUTOR2`'s permissions are always a subset of `DISTRIBUTOR1`'s permissions. It's impossible/invalid for `DISTRIBUTOR2` to have `INCLUDE: CHINA`, for example, because `DISTRIBUTOR1` isn't authorized to do that in the first place. - -If `DISTRIBUTOR2` authorizes `DISTRIBUTOR3` to handle just the city of Hubli, Karnataka, India, for example: -``` -Permissions for DISTRIBUTOR3 < DISTRIBUTOR2 < DISTRIBUTOR1 -INCLUDE: HUBLI-KARNATAKA-INDIA -``` -Again, `DISTRIBUTOR2` cannot authorize `DISTRIBUTOR3` with a region that they themselves do not have access to. - -We've provided a CSV with the list of all countries, states and cities in the world that we know of - please use the data mentioned there for this program. *The codes you see there may be different from what you see here, so please always use the codes in the CSV*. This Readme is only an example. - -Write a program in any language you want (If you're here from Gophercon, use Go :D) that does this. Feel free to make your own input and output format / command line tool / GUI / Webservice / whatever you want. Feel free to hold the dataset in whatever structure you want, but try not to use external databases - as far as possible stick to your langauage without bringing in MySQL/Postgres/MongoDB/Redis/Etc. - -To submit a solution, fork this repo and send a Pull Request on Github. - -For any questions or clarifications, raise an issue on this repo and we'll answer your questions as fast as we can. - - +Run application: +go run main.go + + +Prefix to be used while adding distributors: +ADMIN = "ADMIN" +DISTRIBUTOR_PREFIX = "D00" +SUBDISTRIBUTOR1_PREFIX = "SD01" +SUBDISTRIBUTOR2_PREFIX = "SD02" + + +1) API to add Distributors and Permissions: + +curl --location --request POST 'http://localhost:3001/api/v1/addPermissions' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "distributor":"ADMIN", + "subDistributor":"D00-01", + "include":["IN","BR-AL"], + "exclude":["MH-IN"] + +} +' + +curl --location --request POST 'http://localhost:3001/api/v1/addPermissions' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "distributor":"D00-01", + "subDistributor":"SD01-01", + "include":["MH-IN","KA-IN"], + "exclude":["AL"] + +} +' + +curl --location --request POST 'http://localhost:3001/api/v1/addPermissions' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "distributor":"SD01-01", + "subDistributor":"SD02-01", + "include":["KA-IN"], + "exclude":["ANEKL-KA-IN"] + +} +' + +2) Check if distributor has permission to the region: +curl --location --request GET 'http://localhost:3001/api/v1/isRegionAllowed?distributor=SD02-01®ion=MH-IN' + + +3) Get distributor permission data +curl --location --request GET 'http://localhost:3001/api/v1/permissions?distributor=SD02-01' \ No newline at end of file diff --git a/api/config/constants.go b/api/config/constants.go new file mode 100644 index 000000000..a0b95c55c --- /dev/null +++ b/api/config/constants.go @@ -0,0 +1,14 @@ +package config + +const ( + COUNTRY int = iota + PROVINCE + CITY + + CitiesFilePath = "../cities.csv" + + ADMIN string = "ADMIN" + DISTRIBUTOR_PREFIX string = "D00" + SUBDISTRIBUTOR1_PREFIX string = "SD01" + SUBDISTRIBUTOR2_PREFIX string = "SD02" +) diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 000000000..134257519 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,3 @@ +module distributor-permissions + +go 1.21.1 diff --git a/api/main.go b/api/main.go new file mode 100644 index 000000000..41c118fff --- /dev/null +++ b/api/main.go @@ -0,0 +1,155 @@ +package main + +import ( + "distributor-permissions/config" + service "distributor-permissions/services" + "distributor-permissions/utils" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" +) + +var DR service.DistributorReceiver +var RR service.RegionReciever +var PR service.PermissionsReceiver + +func AddDistributorData(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + payload := utils.RequestPayload{} + encode := json.NewDecoder(r.Body) + err := encode.Decode(&payload) + if err != nil { + log.Printf("Error in loading payload %s\n", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if strings.TrimSpace(payload.Distributor) == "" || strings.TrimSpace(payload.SubDistributor) == "" { + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + switch strings.Split(payload.Distributor, "-")[0] { + case config.ADMIN: + if _, ok := PR.DistributorR.Distributors[payload.SubDistributor]; !ok { + PR.DistributorR.NewDistributor(payload.SubDistributor, payload.Distributor) + } + goto distributor + case config.DISTRIBUTOR_PREFIX: + if _, ok := PR.DistributorR.Distributors[payload.SubDistributor]; !ok { + err := PR.DistributorR.AddSubDistributor(payload.Distributor, payload.SubDistributor) + if err != nil { + log.Printf("Error in adding sub distributor %s\n", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } + goto subDist + case config.SUBDISTRIBUTOR1_PREFIX: + if _, ok := PR.DistributorR.Distributors[payload.Distributor]; ok { + err := PR.DistributorR.AddSubDistributor(payload.Distributor, payload.SubDistributor) + if err != nil { + log.Printf("Error in adding sub distributor2 %s\n", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } + goto subDist + case config.SUBDISTRIBUTOR2_PREFIX: + http.Error(w, "2nd SubDistributor cannot add data", http.StatusBadRequest) + return + default: + http.Error(w, "Incorrect distributor data", http.StatusBadRequest) + return + + } + +distributor: + { + data, err := PR.UpdateDistributorPermissions(payload.SubDistributor, payload.Include, payload.Exclude) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + fmt.Fprintf(w, "%+v", data) + return + } +subDist: + { + data, err := PR.UpdateSubDistributorPermissions(payload.Distributor, payload.SubDistributor, payload.Include, payload.Exclude) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + fmt.Fprintf(w, "%+v", data) + return + } + +} + +func HasRegionPermission(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + distributor := r.URL.Query().Get("distributor") + region := r.URL.Query().Get("region") + if strings.TrimSpace(distributor) == "" || strings.TrimSpace(region) == "" { + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + data := PR.HasRegionPermission(distributor, region) + message := "" + if data { + message = "Distributor has " + region + " permission" + } else { + message = "Distributor doesn't have " + region + " permission" + } + + fmt.Fprintf(w, "Allowed: %v\n Message: %s", data, message) +} + +func GetDistributorData(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + distributor := r.URL.Query().Get("distributor") + data, ok := PR.DistributorPermissions[distributor] + message := "" + if ok { + message = "Distributor " + distributor + " record found" + } else { + message = "Distributor " + distributor + " record not found" + } + fmt.Fprintf(w, "Data: %+v\n Message: %s", data, message) +} + +func main() { + + err := RR.LoadAllRegions() + if err != nil { + log.Println("Error loading regions=>", err) + return + } + + DR.Initilize() + + PR.DistributorPermissions = make(map[string]utils.DistributorData) + PR.RegionR = &RR + PR.DistributorR = &DR + + http.HandleFunc("/api/v1/addPermissions", AddDistributorData) + http.HandleFunc("/api/v1/isRegionAllowed", HasRegionPermission) + http.HandleFunc("/api/v1/permissions", GetDistributorData) + + log.Println("Server listening to port 3001") + http.ListenAndServe(":3001", nil) +} diff --git a/api/services/distributors.go b/api/services/distributors.go new file mode 100644 index 000000000..c80807435 --- /dev/null +++ b/api/services/distributors.go @@ -0,0 +1,28 @@ +package service + +import ( + "distributor-permissions/utils" + "errors" +) + +type DistributorReceiver struct { + Distributors map[string]utils.Distributor +} + +func (dr *DistributorReceiver) Initilize() { + dr.Distributors = make(map[string]utils.Distributor) +} + +func (dr *DistributorReceiver) NewDistributor(name string, parentName string) { + dr.Distributors[name] = utils.Distributor{Name: name, ParentDistributor: parentName, Sub: make(map[string]bool)} +} + +func (dr *DistributorReceiver) AddSubDistributor(dName string, subDName string) error { + if _, ok := dr.Distributors[dName]; ok { + dr.NewDistributor(subDName, dName) + dr.Distributors[dName].Sub[subDName] = true + return nil + } + + return errors.New("distributor Not Found") +} diff --git a/api/services/permission.go b/api/services/permission.go new file mode 100644 index 000000000..941c0f8d6 --- /dev/null +++ b/api/services/permission.go @@ -0,0 +1,270 @@ +package service + +import ( + "distributor-permissions/config" + "distributor-permissions/utils" + "log" + "strings" +) + +type PermissionsReceiver struct { + DistributorPermissions map[string]utils.DistributorData + RegionR *RegionReciever + DistributorR *DistributorReceiver +} + +func (pr *PermissionsReceiver) UpdateDistributorPermissions(distributor string, include, exclude []string) (utils.DistributorData, error) { + if _, ok := pr.DistributorPermissions[distributor]; !ok { + pr.DistributorPermissions[distributor] = utils.DistributorData{Name: distributor, Include: make(map[string]bool), Exclude: make(map[string]bool)} + } + + for _, v := range include { + pr.updatePermissions(v, distributor, true) + } + for _, v := range exclude { + pr.updatePermissions(v, distributor, false) + } + return pr.DistributorPermissions[distributor], nil +} + +func (pr *PermissionsReceiver) updatePermissions(regionCode string, distributor string, include bool) { + list := strings.Split(regionCode, "-") + listLen := len(list) + if listLen == 1 { + // list[0] is country code + country, ok := pr.RegionR.IsCountryAllowed(strings.TrimSpace(list[0])) + if !ok { + log.Printf("Country %s is not available\n", list[0]) + return + } + if include { + pr.AddRegion(distributor, country.Code, include) + } else { + pr.AddRegion(distributor, country.Code, include) + + //delete province+cities entries of country + suffix := "-" + country.Code + pr.DeleteRegion(distributor, suffix, true) + } + } + + if listLen == 2 { + // list[0] is province code + // list[1] is country code + cdata, pData, ok := pr.RegionR.IsProvinceAllowed(strings.TrimSpace(list[1]), strings.TrimSpace(list[0])) + if !ok { + log.Printf("Country %s with province %s is not available\n", list[1], list[0]) + return + } + regionCode := pData.Code + "-" + cdata.Code + if include { + if ok := pr.IsRegionExcluded(distributor, cdata.Code); ok { + log.Printf("Country %s is excluded\n", list[1]) + return + } + pr.AddRegion(distributor, regionCode, include) + } else { + pr.AddRegion(distributor, regionCode, include) + //delete city entries of the province + suffix := "-" + regionCode + pr.DeleteRegion(distributor, suffix, true) + } + } + + if listLen == 3 { + //list[0] is city code + // list[1] is province code + // list[2] is country code + countryData, pData, cityData, ok := pr.RegionR.IsCityAllowed(list[2], list[1], list[0]) + if !ok { + log.Printf("Country %s with province %s and city %s is not available\n", list[2], list[1], list[0]) + return + } + regionCode := cityData.Code + "-" + pData.Code + "-" + countryData.Code + if include { + if ok := pr.IsRegionExcluded(distributor, countryData.Code); ok { + log.Printf("Country %s is excluded\n", list[2]) + return + } + if ok := pr.IsRegionExcluded(distributor, pData.Code+"-"+countryData.Code); ok { + log.Printf("Country %s and province %s is excluded\n", list[2], list[1]) + return + } + pr.AddRegion(distributor, regionCode, include) + } else { + pr.AddRegion(distributor, regionCode, include) + } + + } +} + +func (pr *PermissionsReceiver) AddRegion(distributor, region string, include bool) { + if include { + pr.DistributorPermissions[distributor].Include[region] = true + delete(pr.DistributorPermissions[distributor].Exclude, region) + } else { + pr.DistributorPermissions[distributor].Exclude[region] = true + delete(pr.DistributorPermissions[distributor].Include, region) + } +} + +func (pr *PermissionsReceiver) DeleteRegion(distributor, suffix string, fromInclude bool) { + if fromInclude { + for regionCode := range pr.DistributorPermissions[distributor].Include { + if strings.HasSuffix(regionCode, suffix) { + delete(pr.DistributorPermissions[distributor].Include, regionCode) + } + } + } +} + +func (pr *PermissionsReceiver) IsRegionExcluded(distributor, region string) bool { + _, ok := pr.DistributorPermissions[distributor].Exclude[region] + return ok +} + +func (pr *PermissionsReceiver) HasRegionPermission(distributor, regionCode string) bool { + list := strings.Split(regionCode, "-") + listLen := len(list) + switch listLen { + case 1: //done + country := strings.TrimSpace(list[0]) + //IsExcluded + if _, ok := pr.DistributorPermissions[distributor].Exclude[country]; ok { + return false + } + //IsIncluded + if _, ok := pr.DistributorPermissions[distributor].Include[country]; ok { + return true + } + + //includes any data under country + suffix := "-" + country + for regionCode := range pr.DistributorPermissions[distributor].Include { + if strings.HasSuffix(regionCode, suffix) { + return true + } + } + case 2: + country := strings.TrimSpace(list[1]) + province := strings.TrimSpace(list[0]) + pcCode := province + "-" + country + + //IsExcluded + //province level check + if _, ok := pr.DistributorPermissions[distributor].Exclude[pcCode]; ok { + return false + } + //country level check + if _, ok := pr.DistributorPermissions[distributor].Exclude[country]; ok { + return false + } + + //IsIncluded + //province level + if _, ok := pr.DistributorPermissions[distributor].Include[pcCode]; ok { + return true + } + //includes any city under province + suffix := "-" + pcCode + for regionCode := range pr.DistributorPermissions[distributor].Include { + if strings.HasSuffix(regionCode, suffix) { + return true + } + } + //country level + if _, ok := pr.DistributorPermissions[distributor].Include[country]; ok { + return true + } + case 3: + country := strings.TrimSpace(list[2]) + province := strings.TrimSpace(list[1]) + city := strings.TrimSpace(list[0]) + rCode := city + "-" + province + "-" + country + pCode := province + "-" + country + //Is excluded + //city level check + if _, ok := pr.DistributorPermissions[distributor].Exclude[rCode]; ok { + return false + } + //province level check + if _, ok := pr.DistributorPermissions[distributor].Exclude[pCode]; ok { + return false + } + //country check + if _, ok := pr.DistributorPermissions[distributor].Exclude[country]; ok { + return false + } + + //IsIncluded + //city level check + if _, ok := pr.DistributorPermissions[distributor].Include[rCode]; ok { + return true + } + //province level check + if _, ok := pr.DistributorPermissions[distributor].Include[pCode]; ok { + return true + } + //country check + if _, ok := pr.DistributorPermissions[distributor].Include[country]; ok { + return true + } + default: + return false + } + return false +} + +func (pr *PermissionsReceiver) UpdateSubDistributorPermissions(distributor, subDistributor string, include, exclude []string) (utils.DistributorData, error) { + //add subDistributor if not exists + if _, ok := pr.DistributorPermissions[subDistributor]; !ok { + pr.DistributorPermissions[subDistributor] = utils.DistributorData{Name: subDistributor, Include: make(map[string]bool), Exclude: make(map[string]bool)} + } + + //check if the distributor is subDistributor1 + if strings.HasPrefix(distributor, config.SUBDISTRIBUTOR1_PREFIX) { + //find the parent of subDistributor1 + parent := pr.DistributorR.Distributors[distributor].ParentDistributor + + for _, v := range include { + pr.updateSubDistributor2Permission(parent, distributor, subDistributor, v, true) + } + + for _, v := range exclude { + pr.updateSubDistributor2Permission(parent, distributor, subDistributor, v, false) + } + } else if strings.HasPrefix(distributor, config.DISTRIBUTOR_PREFIX) { + for _, v := range include { + pr.updateSubDistributor1Permission(distributor, subDistributor, v, true) + } + + for _, v := range exclude { + pr.updateSubDistributor1Permission(distributor, subDistributor, v, false) + } + } + return pr.DistributorPermissions[subDistributor], nil +} + +func (pr *PermissionsReceiver) updateSubDistributor2Permission(parent, subDistributor1, subDistributor2, region string, include bool) { + //check if subDistributor has the permission to include the region + subPermission := pr.HasRegionPermission(subDistributor1, region) + //check if the parentDistributor has the permission to include the region + parentPermission := pr.HasRegionPermission(parent, region) + + //if both are allowed, update the subDistributor2 list + if subPermission && parentPermission { + pr.updatePermissions(region, subDistributor2, include) + } else { + log.Printf("Region %s Excluded in parent distributors of %s\n", region, subDistributor2) + } +} + +func (pr *PermissionsReceiver) updateSubDistributor1Permission(distributor, subDistributor, region string, include bool) { + //check if distributor has the permission to include the region + allowed := pr.HasRegionPermission(distributor, region) + if allowed { + pr.updatePermissions(region, subDistributor, include) + } else { + log.Printf("Region %s Excluded in parent distributors of %s\n", region, subDistributor) + } +} diff --git a/api/services/region.go b/api/services/region.go new file mode 100644 index 000000000..2f1f5cff8 --- /dev/null +++ b/api/services/region.go @@ -0,0 +1,79 @@ +package service + +import ( + "distributor-permissions/config" + "distributor-permissions/utils" + "encoding/csv" + "io" + "log" + "os" + "strings" +) + +type RegionReciever struct { + AllRegions map[string]utils.RegionsList +} + +func (rr *RegionReciever) LoadAllRegions() error { + + file, err := os.Open(config.CitiesFilePath) + if err != nil { + log.Printf("File :services/region.go\nFunc: LoadAllRegions\nInfo: Error while opening cities.csv file\nError:%v\n", err) + return nil + } + defer file.Close() + csvReader := csv.NewReader(file) + if csvReader == nil { + log.Printf("File :services/region.go\nFunc: LoadAllRegions\nInfo: No data in cities.csv file\nError: no data \n") + return nil + } + rr.AllRegions = map[string]utils.RegionsList{} + for { + data, err := csvReader.Read() + if err == io.EOF { + break + } + if err != nil { + log.Printf("File :services/region.go\nFunc: LoadAllRegions\nInfo: Error reading record from file\nError: %v \n", err) + return nil + } + cityCode := strings.ToUpper(data[0]) + provinceCode := strings.ToUpper(data[1]) + countryCode := strings.ToUpper(data[2]) + city := strings.ToUpper(data[3]) + province := strings.ToUpper(data[4]) + country := strings.ToUpper(data[5]) + + if _, ok := rr.AllRegions[countryCode]; !ok { + rr.AllRegions[countryCode] = utils.RegionsList{Code: countryCode, Name: country, Type: config.COUNTRY, Regions: make(map[string]utils.RegionsList)} + } + if _, ok := rr.AllRegions[countryCode].Regions[provinceCode]; !ok { + rr.AllRegions[countryCode].Regions[provinceCode] = utils.RegionsList{Code: provinceCode, Name: province, Type: config.PROVINCE, Regions: make(map[string]utils.RegionsList)} + } + rr.AllRegions[countryCode].Regions[provinceCode].Regions[cityCode] = utils.RegionsList{Code: cityCode, Name: city, Type: config.CITY} + } + return nil +} + +func (rr *RegionReciever) IsCountryAllowed(country string) (utils.Region, bool) { + data, ok := rr.AllRegions[country] + return utils.Region{Name: data.Name, Code: data.Code, Type: data.Type}, ok +} + +func (rr *RegionReciever) IsProvinceAllowed(country string, province string) (utils.Region, utils.Region, bool) { + cData, cok := rr.IsCountryAllowed(country) + if cok { + pData, pok := rr.AllRegions[country].Regions[province] + return cData, utils.Region{Name: pData.Name, Code: pData.Code, Type: pData.Type}, cok && pok + } + return utils.Region{}, utils.Region{}, false +} + +func (rr *RegionReciever) IsCityAllowed(country, province, city string) (utils.Region, utils.Region, utils.Region, bool) { + countryData, pData, cpOk := rr.IsProvinceAllowed(country, province) + if cpOk { + cData, cok := rr.AllRegions[country].Regions[province].Regions[city] + return countryData, pData, utils.Region{Name: cData.Name, Code: cData.Code, Type: cData.Type}, cok && cpOk + } + return utils.Region{}, utils.Region{}, utils.Region{}, false +} diff --git a/api/utils/types.go b/api/utils/types.go new file mode 100644 index 000000000..1269cf81b --- /dev/null +++ b/api/utils/types.go @@ -0,0 +1,33 @@ +package utils + +type RegionsList struct { + Name string + Type int + Code string + Regions map[string]RegionsList +} + +type DistributorData struct { + Name string `json:"name"` + Include map[string]bool `json:"include"` + Exclude map[string]bool `json:"exclude"` +} + +type Region struct { + Name string + Type int + Code string +} + +type Distributor struct { + Name string + ParentDistributor string + Sub map[string]bool +} + +type RequestPayload struct { + Distributor string `json:"distributor"` + SubDistributor string `json:"subDistributor"` + Include []string `json:"include"` + Exclude []string `json:"exclude"` +}