diff --git a/README.md b/README.md index f1c342f65..f921925d5 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,73 @@ 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. +## Solution Description +The service starts an HTTP server to listen to incoming requests for creating distributors. It expects to receive the distributor data from POST request in the JSON format as given below: +``` +{ + "name": "DISTRIBUTOR2", + "inherits": "DISTRIBUTOR1", + "include": ["IN"], + "exclude": ["JK:IN"] +} +``` +The location codes follow the format `::`. To include the entire province or country, codes such as `:` or `` could be used. + +The service can handle GET and POST requests currently, to perform the following operations: +1. GET requests: Can be used to query whether a distributor has access to a certain location code. +2. POST requests: Can be used to create new distributors. It returns a JSON response containing the data it has stored. It weeds out any locations the distributor cannot have access to. + +In future following requests could also be implemented: +1. PATCH request: Can be used to update the include or exclude lists of a distributor +2. PUT request: Can be used to replace the existing distributor + + +### Running the service locally +To run the service locally, use the command: +``` +go run all +``` + +The following message will be visible on the terminal if the server starts successfully: +``` +challenge2016>go run all +Starting server on :8080... +``` + +### Limitations: +Currently, a distributor cannot be updated once it is created via POST request. + +### GET request example +``` +curl "http://localhost:8080/distributor?distributor=DISTRIBUTOR1&location=UDHAP:JK:IN" +``` + +### POST request example +``` +curl -X POST http://localhost:8080/distributor -H "Content-Type: application/json" -d '{ + "name": "DISTRIBUTOR1", + "include": ["IN", "WS"], + "exclude": ["UP:IN", "YAVTM:MH:IN"] +}' +``` + +### Expected responses +StatusOK - 200 : Distributor can access the given location +StatusCreated - 201 : Successfully created Distributor +StatusNotFound - 404 : Distributor not found +StatusForbidden - 403 : Distributor cannot access the given location +StatusBadRequest - 400 : Received invalid JSON input +StatusConflict - 409 : Distributor already exists +StatusInternalServerError - 500 : Error occurred while returning response + +## Run Unit tests + +Unit tests can be run using the command: +go test + +## Bulk/Stress test: + +The script test.py can be used as follows: +python3 test.py +This script can be used to generate a chain of 50 distributors by default. Script can be modified to test more number of distributors or increasing the number of cities being included by the distributors. \ No newline at end of file diff --git a/constants.go b/constants.go new file mode 100644 index 000000000..9d90d7013 --- /dev/null +++ b/constants.go @@ -0,0 +1,13 @@ +package main + +const ( + ErrDistributorNotExist = "Error: distributor does not exist" + ErrDistributorExists = "Error: distributor already exists" + ErrInvalidJSONPayload = "Invalid JSON payload" + ErrInvalidJSONResponse = "Invalid JSON response" + ErrMissingParams = "Missing 'distributor' or 'location' parameter" + ErrMethodNotAllowed = "Only GET & POST methods are allowed" + MsgDistributorCreated = "Distributor created successfully" + MsgAccessGranted = "YES" + MsgAccessDenied = "NO" +) diff --git a/distributor.go b/distributor.go new file mode 100644 index 000000000..6dfe02909 --- /dev/null +++ b/distributor.go @@ -0,0 +1,52 @@ +package main + +// Request holds the input data +type Request struct { + Name string `json:"name,require"` + Include []string `json:"include"` + Exclude []string `json:"exclude"` + Inherits string `json:"inherits,omitempty"` +} + +// Distributor holds the permissions for each distributor +type Distributor struct { + Name string + Include []string + Exclude []string + Inherits string +} + +var distributorDB = make(map[string]*Distributor) + +// createDistributor creates a new distributor or clones from a parent distributor if inheritance is defined. +func (d *Request) createDistributor() *Distributor { + if d.Inherits != "" { + // If Inherits is set, we clone from the parent distributor + parent := distributorDB[d.Inherits] + return parent.clone(d) + } + // If no inheritance, create a new distributor + return &Distributor{ + Name: d.Name, + Include: d.Include, + Exclude: d.Exclude, + Inherits: d.Inherits, // Inheritance remains unchanged + } +} + +// clone creates a new distributor based on the parent's permissions and merges the include/exclude lists +func (d *Distributor) clone(inData *Request) *Distributor { + var includeList []string + for _, code := range inData.Include { + res,_ := checkDistributor(d.Name, code) + if res == MsgAccessGranted { + includeList = append(includeList, code) + } + } + return &Distributor{ + Name: inData.Name, + Include: includeList, + Exclude: append(d.Exclude, inData.Exclude...), + Inherits: inData.Inherits, // Inheritance remains unchanged + } +} diff --git a/distributor_test.go b/distributor_test.go new file mode 100644 index 000000000..23b3d0eb3 --- /dev/null +++ b/distributor_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "testing" +) + +func TestCreateDistributor(t *testing.T) { + // Sample distributor data + request := &Request{ + Name: "test-distributor", + Include: []string{"city:province:country"}, + Exclude: []string{"city:province:anotherCountry"}, + Inherits: "", + } + + // Create distributor from request + distributor := request.createDistributor() + + // Check that distributor is not nil + if distributor == nil { + t.Fatalf("Expected distributor to be created, but got nil") + } + + // Check if the distributor has the correct name + if distributor.Name != request.Name { + t.Errorf("Expected distributor name to be '%s', but got '%s'", request.Name, distributor.Name) + } + + // Check the Include and Exclude lists + if len(distributor.Include) != 1 || distributor.Include[0] != "city:province:country" { + t.Errorf("Expected include list to have 1 item: 'city:province:country', but got %v", distributor.Include) + } + + if len(distributor.Exclude) != 1 || distributor.Exclude[0] != "city:province:anotherCountry" { + t.Errorf("Expected exclude list to have 1 item: 'city:province:anotherCountry', but got %v", distributor.Exclude) + } +} + +func TestCloneDistributor(t *testing.T) { + // Sample parent distributor + parent := &Distributor{ + Name: "parent-distributor", + Include: []string{"parentCountry"}, + Exclude: []string{"excludeCountry"}, + Inherits: "", + } + + // Sample request to clone + request := &Request{ + Name: "child-distributor", + Include: []string{"city:province:parentCountry"}, + Exclude: []string{"city:province:excludeCountry"}, + Inherits: "parent-distributor", + } + + // Add parent to the global distributorDB + distributorDB["parent-distributor"] = parent + + // Create distributor from request (it should clone from parent) + distributor := request.createDistributor() + + // Check that the distributor has inherited the parent's data + if len(distributor.Include) != 1 || distributor.Include[0] != "city:province:parentCountry" { + t.Errorf("Expected include list to have 'city:province:parentCountry', but got %v", distributor.Include) + } + + if len(distributor.Exclude) != 2 || distributor.Exclude[0] != "excludeCountry" || distributor.Exclude[1] != "city:province:excludeCountry" { + t.Errorf("Expected exclude list to have both 'city:province:excludeCountry', but got %v", distributor.Exclude) + } + + // Check inheritance + if distributor.Inherits != "parent-distributor" { + t.Errorf("Expected distributor to inherit from 'parent-distributor', but got '%s'", distributor.Inherits) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..d167ee59c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module challenge2016 + +go 1.21.1 diff --git a/main.go b/main.go new file mode 100644 index 000000000..9f43ab9d3 --- /dev/null +++ b/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func startServer(){ + http.HandleFunc("/distributor", requestHandler) + + fmt.Println("Starting server on :8080...") + err := http.ListenAndServe(":8080", nil) + if err != nil { + log.Fatalf("Server failed to start: %v", err) + } +} + +// Main driver logic +func main() { + + startServer() + +} \ No newline at end of file diff --git a/request.go b/request.go new file mode 100644 index 000000000..5bcafc29e --- /dev/null +++ b/request.go @@ -0,0 +1,153 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "strings" + "sync" +) + +var distributorMu sync.RWMutex + +// split the given location code. Expected format: city:province:country +func getLocation(location string) (string, string, string) { + parts := strings.Split(location, ":") + var city, province, country string + + switch len(parts) { + case 3: + city = parts[0] + province = parts[1] + country = parts[2] + case 2: + province = parts[0] + country = parts[1] + case 1: + country = parts[0] + } + return city, province, country +} + +// Checks if a distributor has access to the given location code +func checkDistributor(distributor string, location string) (string, int) { + log.Printf("Checking distributor '%s' for location '%s'", distributor, location) + gcity, gprovince, gcountry := getLocation(location) + + distributorMu.RLock() + if _, ok := distributorDB[distributor]; !ok { + distributorMu.RUnlock() + log.Printf("Distributor '%s' not found", distributor) + return ErrDistributorNotExist, http.StatusNotFound + } + distributorMu.RUnlock() + + d := distributorDB[distributor] + includeList := d.Include + excludeList := d.Exclude + + for _, code := range excludeList { + city, province, country := getLocation(code) + if country == gcountry { + if province == "" || (province == gprovince && (city == "" || city == gcity)) { + log.Printf("Distributor '%s' not authorized for location '%s'", distributor, location) + return MsgAccessDenied, http.StatusForbidden + } + } + } + + for _, code := range includeList { + city, province, country := getLocation(code) + if country == gcountry { + if province == "" || (province == gprovince && (city == "" || city == gcity)) { + log.Printf("Distributor '%s' authorized for location '%s'", distributor, location) + return MsgAccessGranted, http.StatusOK + } + } + } + + log.Printf("Distributor '%s' not authorized for location '%s'", distributor, location) + return MsgAccessDenied, http.StatusForbidden +} + +// Handles GET requests +func getHandler(distributor string, location string) (string, int) { + log.Printf("Handling GET request for distributor '%s' at location '%s'", distributor, location) + if distributor == "" || location == "" { + log.Println("Missing 'distributor' or 'location' parameter") + return ErrMissingParams, http.StatusBadRequest + } + + return checkDistributor(distributor, location) +} + +// Processes POST request, to create a distributor +func postProcessing(distributor *Request) (string, int, *Distributor) { + log.Printf("Processing POST request for distributor '%s'", distributor.Name) + distributorMu.RLock() + if _, ok := distributorDB[distributor.Name]; ok { + distributorMu.RUnlock() + log.Printf("Distributor '%s' already exists", distributor.Name) + // TODO return existing distributor + return ErrDistributorExists, http.StatusConflict, distributorDB[distributor.Name] + } + distributorMu.RUnlock() + + newDistributor := distributor.createDistributor() + + distributorMu.Lock() + distributorDB[distributor.Name] = newDistributor + distributorMu.Unlock() + log.Printf("Distributor '%s' created successfully", distributor.Name) + + return MsgDistributorCreated, http.StatusCreated, newDistributor +} + +// Handles POST requests +func postHandler(r *http.Request) (string, int) { + var distributor *Request + err := json.NewDecoder(r.Body).Decode(&distributor) + if err != nil { + log.Printf("Invalid JSON payload: %v", err) + return ErrInvalidJSONPayload, http.StatusBadRequest + } + + response, status, dist := postProcessing(distributor) + if response != MsgDistributorCreated && response != ErrDistributorExists{ + return response, status + } + + // Encode the distributor object as JSON to return in response + responseBytes, err := json.Marshal(dist) + if err != nil { + return ErrInvalidJSONResponse, http.StatusInternalServerError + } + + return string(responseBytes), status +} + +// Handles incoming requests and returns responses +func requestHandler(w http.ResponseWriter, r *http.Request) { + var response string + var status int + + switch r.Method { + case http.MethodGet: + distributor := r.URL.Query().Get("distributor") + location := r.URL.Query().Get("location") + log.Printf("Received GET request with distributor='%s', location='%s'", distributor, location) + response, status = getHandler(distributor, location) + + case http.MethodPost: + response, status = postHandler(r) + + default: + log.Printf("Method '%s' not allowed", r.Method) + response = ErrMethodNotAllowed + status = http.StatusMethodNotAllowed + } + + log.Printf("Response status: %d, response body: %s", status, response) + w.WriteHeader(status) + w.Write([]byte(response)) +} diff --git a/request_test.go b/request_test.go new file mode 100644 index 000000000..ec25d3609 --- /dev/null +++ b/request_test.go @@ -0,0 +1,262 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + "bytes" + "encoding/json" +) + +func TestGetLocation(t *testing.T) { + tests := []struct { + input string + expected struct { + city, province, country string + } + }{ + {"city:province:country", struct{ city, province, country string }{"city", "province", "country"}}, + {"province:country", struct{ city, province, country string }{"", "province", "country"}}, + {"country", struct{ city, province, country string }{"", "", "country"}}, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + city, province, country := getLocation(test.input) + + if city != test.expected.city || province != test.expected.province || country != test.expected.country { + t.Errorf("For input '%s', expected (city=%s, province=%s, country=%s), but got (city=%s, province=%s, country=%s)", + test.input, test.expected.city, test.expected.province, test.expected.country, city, province, country) + } + }) + } +} + +func TestCheckDistributor(t *testing.T) { + // Sample distributor + distributorDB["distributor1"] = &Distributor{ + Name: "distributor1", + Include: []string{"city:province:country"}, + Exclude: []string{"city:province:anotherCountry"}, + } + + // Test cases + tests := []struct { + distributor string + location string + expected string + }{ + {"distributor1", "city:province:country", MsgAccessGranted}, + {"distributor1", "city:province:anotherCountry", MsgAccessDenied}, + {"distributor1", "unknownCity:unknownProvince:unknownCountry", MsgAccessDenied}, + {"nonExistent", "city:province:country", ErrDistributorNotExist}, + } + + for _, test := range tests { + t.Run(test.distributor+" "+test.location, func(t *testing.T) { + result,_ := checkDistributor(test.distributor, test.location) + if result != test.expected { + t.Errorf("Expected '%s', but got '%s' for distributor '%s' and location '%s'", + test.expected, result, test.distributor, test.location) + } + }) + } +} + +func TestGetHandler(t *testing.T) { + // Clear distributorDB before each test to avoid interference between tests + distributorDB = make(map[string]*Distributor) + + // Mock data for distributors + distributorDB["test-distributor"] = &Distributor{ + Name: "test-distributor", + Include: []string{"city:province:country"}, + Exclude: []string{"city:province:excludeCountry"}, + } + + tests := []struct { + name string + distributor string + location string + expectedBody string + expectedStatus int + }{ + { + name: "Valid request", + distributor: "test-distributor", + location: "city:province:country", + expectedBody: MsgAccessGranted, // "YES" + expectedStatus: http.StatusOK, // 200 OK + }, + { + name: "Location not included", + distributor: "test-distributor", + location: "city:province:excludeCountry", + expectedBody: MsgAccessDenied, // "NO" + expectedStatus: http.StatusForbidden, // 200 OK (not 403) + }, + { + name: "Missing distributor", + distributor: "", + location: "city:province:country", + expectedBody: ErrMissingParams, // "Missing 'distributor' or 'location' parameter" + expectedStatus: http.StatusBadRequest, // 400 Bad Request + }, + { + name: "Missing location", + distributor: "test-distributor", + location: "", + expectedBody: ErrMissingParams, // "Missing 'distributor' or 'location' parameter" + expectedStatus: http.StatusBadRequest, // 400 Bad Request + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Call the getHandler with the mock data + body, status := getHandler(tt.distributor, tt.location) + + // Check if the status code matches the expected value + if status != tt.expectedStatus { + t.Errorf("Expected status %v but got %v", tt.expectedStatus, status) + } + + // Check if the response body matches the expected value + if body != tt.expectedBody { + t.Errorf("Expected body '%v' but got '%v'", tt.expectedBody, body) + } + }) + } +} + +func TestPostHandler(t *testing.T) { + // Clear distributorDB before each test to avoid interference between tests + distributorDB = make(map[string]*Distributor) + + tests := []struct { + name string + payload *Request + expectedStatus int + expectedBody interface{} // to allow flexibility for string or struct comparison + }{ + { + name: "Valid request to create distributor", + payload: &Request{ + Name: "new-distributor", + Include: []string{"parentCountry"}, + Exclude: []string{"city:province:excludeCountry"}, + Inherits: "", + }, + expectedBody: &Distributor{ + Name: "new-distributor", + Include: []string{"parentCountry"}, + Exclude: []string{"city:province:excludeCountry"}, + Inherits: "", + }, + expectedStatus: http.StatusCreated, // 201 Created + }, + { + name: "Valid request to create distributor with parent", + payload: &Request{ + Name: "child-distributor", + Include: []string{"city:province:parentCountry","city:province:excludeCountry"}, + Exclude: []string{}, + Inherits: "new-distributor", + }, + expectedBody: &Distributor{ + Name: "child-distributor", + Include: []string{"city:province:parentCountry"}, + Exclude: []string{"city:province:excludeCountry"}, + Inherits: "new-distributor", + }, + expectedStatus: http.StatusCreated, // 201 Created + }, + { + name: "Distributor already exists", + payload: &Request{ + Name: "new-distributor", // Name already exists + Include: []string{"city:province:parentCountry"}, + Exclude: []string{"city:province:excludeCountry"}, + Inherits: "", + }, + expectedBody: &Distributor{ + Name: "new-distributor", + Include: []string{"parentCountry"}, + Exclude: []string{"city:province:excludeCountry"}, + Inherits: "", + }, + expectedStatus: http.StatusConflict, // 409 Conflict + }, + { + name: "Invalid JSON payload", + payload: nil, // Invalid payload (nil) + expectedBody: ErrInvalidJSONPayload, // "Invalid JSON payload" + expectedStatus: http.StatusBadRequest, // 400 Bad Request + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Prepare the JSON payload + var jsonPayload []byte + if tt.payload != nil { + var err error + jsonPayload, err = json.Marshal(tt.payload) + if err != nil { + t.Fatalf("Failed to marshal payload: %v", err) + } + } + + // Create a request using httptest.NewRequest + req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewBuffer(jsonPayload)) + + // Call the postHandler and capture the return values + body, status := postHandler(req) + + // Check if the status code matches the expected value + if status != tt.expectedStatus { + t.Errorf("Expected status %v but got %v", tt.expectedStatus, status) + } + + // Check if the response body matches the expected value + switch expectedBody := tt.expectedBody.(type) { + case string: + // Compare as string for error messages + if body != expectedBody { + t.Errorf("Expected body '%v' but got '%v'", expectedBody, body) + } + case *Distributor: + // Decode JSON response and compare as struct for successful creation + if status == http.StatusCreated { + var actualDistributor Distributor + err := json.Unmarshal([]byte(body), &actualDistributor) + if err != nil { + t.Fatalf("Failed to unmarshal JSON response: %v", err) + } + + // Compare distributor attributes + if actualDistributor.Name != expectedBody.Name || + !equalSlices(actualDistributor.Include, expectedBody.Include) || + !equalSlices(actualDistributor.Exclude, expectedBody.Exclude) || + actualDistributor.Inherits != expectedBody.Inherits { + t.Errorf("Expected distributor %v but got %v", expectedBody, actualDistributor) + } + } + } + }) + } +} + +// Helper function to compare slices +func equalSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + diff --git a/test.py b/test.py new file mode 100644 index 000000000..7edb6fd5b --- /dev/null +++ b/test.py @@ -0,0 +1,154 @@ +import csv +import requests +import random +import json + +BASE_URL = 'http://localhost:8080' # server's URL + +country_db = {} +distributor_db = {} + +# Generate list of locations for distributors with no parents +def generate_random_list(): + # Generate upto 100 random locations + list_length = random.randint(1, 100) + locations_set = set() + + while len(locations_set) < list_length: + country_code = random.choice(list(country_db.keys())) + province_code = random.choice(list(country_db[country_code].keys())) + city_code = random.choice(list(country_db[country_code][province_code].keys())) + choice = random.choice([1, 2, 3]) + + if choice == 1: + location = f"{city_code}:{province_code}:{country_code}" + elif choice == 2: + location = f"{province_code}:{country_code}" + else: + location = country_code + + locations_set.add(location) + + return list(locations_set) + +# Generate list from include list of distributor or it's parent +def generate_from_list(include_list): + matched_locations = [] + k = random.randint(1, len(include_list)) + sub_list = random.sample(include_list, k) + + for location in sub_list: + parts = location.split(":") + + if len(parts) == 1: + country = parts[0] + if country in country_db: + province = random.choice(list(country_db[country].keys())) + city = random.choice(list(country_db[country][province].keys())) + matched_locations.append(f"{city}:{province}:{country}") + + elif len(parts) == 2: + province, country = parts + if country in country_db and province in country_db[country]: + city = random.choice(list(country_db[country][province].keys())) + matched_locations.append(f"{city}:{province}:{country}") + + elif len(parts) == 3: + matched_locations.append(location) + + return matched_locations + +# Test GET and POST requests by creating distributors in bulk +def bulk_test_get_and_post(): + NUM_DISTRIBUTORS = 50 + for i in range(1, NUM_DISTRIBUTORS+1): + distributor_name = f"distributor-{i}" + + # Determine the range for selecting parent distributors + if i <= 10: + include_list = generate_random_list() + else: + # Calculate the previous range + min_parent = ((i - 1) // 10 - 1) * 10 + 1 + max_parent = min_parent + 9 + # Select a random parent from previous range + parent_num = random.randint(min_parent, max_parent) + parent_name = f"distributor-{parent_num}" + + # Ensure the parent distributor exists + if parent_name in distributor_db: + parent_list = distributor_db[parent_name].get("Include", []) + include_list = generate_from_list(parent_list) + else: + include_list = generate_random_list() + + exclude_list = generate_from_list(include_list) + exclude_list = [code for code in exclude_list if code not in include_list] + + print(f"Distributor Name: {distributor_name}") + print("Include List:", include_list) + print("Exclude List:", exclude_list) + + response_text = test_post_request(distributor_name, include_list, exclude_list) + response_dict = json.loads(response_text) + distributor_db[distributor_name] = response_dict + + include_list = response_dict.get("Include", []) + exclude_list = response_dict.get("Exclude", []) + filtered_include_list = [code for code in include_list if code not in exclude_list] + + if filtered_include_list: + location_code = random.choice(filtered_include_list) + response = test_get_request(distributor_name, location_code) + assert response.status_code == 200 + assert response.text == "YES" + + if exclude_list: + location_code = random.choice(exclude_list) + response = test_get_request(distributor_name, location_code) + assert response.status_code == 403 + assert response.text == "NO" + +def test_post_request(distributor_name, include_list, exclude_list): + print(f"POST Request: {distributor_name}, Include={include_list}, Exclude={exclude_list}") + request_data = { + "name": distributor_name, + "include": include_list, + "exclude": exclude_list, + "inherits": "" + } + response = requests.post(f"{BASE_URL}/distributor", json=request_data) + return response.text + +def test_get_request(distributor, location_code): + response = requests.get(f"{BASE_URL}/distributor?distributor={distributor}&location={location_code}") + print(f"GET Request: {distributor}, Location={location_code}, Response:{response.text}") + return response + +def parse_db(data): + for row in data: + if row: + city_code, city = row[0], row[3] + province_code = row[1] + country_code = row[2] + + if country_code not in country_db: + country_db[country_code] = {} + + if province_code not in country_db[country_code]: + country_db[country_code][province_code] = {} + + country_db[country_code][province_code][city_code] = city + +def main(): + csv_file_path = 'cities.csv' + with open(csv_file_path, newline='', encoding='utf-8') as csvfile: + csvreader = csv.reader(csvfile) + header = next(csvreader) + data = [row for row in csvreader if row] + + parse_db(data) + bulk_test_get_and_post() + +if __name__ == "__main__": + main() diff --git a/utils.go b/utils.go new file mode 100644 index 000000000..b154f1f9f --- /dev/null +++ b/utils.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "os" + "log" + "encoding/json" +) + +func readJSON(name string) Distributor{ + file, err := os.Open(name) + defer file.Close() + if err != nil{ + log.Fatal("Error while reading file", err) + } + // Decode JSON data from the file + var data Distributor + decoder := json.NewDecoder(file) + err = decoder.Decode(&data) + if err != nil { + log.Fatalf("Error decoding JSON: %v", err) + } + fmt.Printf("%+v\n", data) + + return data +}