diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8f11f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work \ No newline at end of file diff --git a/degrees.exe b/degrees.exe new file mode 100644 index 0000000..79fb99f Binary files /dev/null and b/degrees.exe differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7b2f58c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/challenge2015 + +go 1.21.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..687397b --- /dev/null +++ b/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "os" + "github.com/challenge2015/util" + "github.com/challenge2015/service" +) + +func main() { + + if len(os.Args) < 3 { + fmt.Println("Error: Invalid number of arguments. Please provide two actor names.") + fmt.Println("Usage: degrees ") + return + } + + firstActorName := os.Args[1] + secondActorName := os.Args[2] + + if !util.IsValidURL(firstActorName) || !util.IsValidURL(secondActorName) { + fmt.Println("Error: Invalid URL format. URLs should be in the format 'actor-name' or 'director-name' separated by a hyphen.") + return + } + + fmt.Println("First actor name: ", firstActorName) + fmt.Println("Second actor name: ", secondActorName) + + service.DisplayResult(firstActorName, secondActorName) +} \ No newline at end of file diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..c621090 --- /dev/null +++ b/service/service.go @@ -0,0 +1,194 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + // "os" + "runtime" + "sync" +) + +type queue struct { + URL string + degree int + path []string +} + +type BufferData struct { + URL string `json:"url"` + Role string `json:"role"` + Name string `json:"name"` +} + +type BufferMetaData struct { + URL string `json:"url"` + Type string `json:"type"` + Name string `json:"name"` +} + +type Person struct { + BufferMetaData + Movies []BufferData `json:"movies"` +} + +type Movie struct { + BufferMetaData + Cast []BufferData `json:"cast"` +} + +type FetchResponse interface { + urlType() string +} + +func (person Person) urlType() string { + return person.Type +} + +func (movie Movie) urlType() string { + return movie.Type +} + +const ( + CategoryPerson = iota + CategoryMovie +) + +func fetchAPIData(url string, URLCategiry int) (FetchResponse, error) { + // fmt.Println("Fetching data for URL: ", "https://data.moviebuff.com/" + url) + resp, err := http.Get("https://data.moviebuff.com/" + url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + responseData, err := io.ReadAll(resp.Body) + // fmt.Println("Response data: ", string(responseData)) + if err != nil { + return nil, err + } + + if URLCategiry == CategoryPerson { + actor := Person{} + if err := json.Unmarshal(responseData, &actor); err != nil { + return nil, err + } + return actor, nil + } + + movie := Movie{} + if err := json.Unmarshal(responseData, &movie); err != nil { + return nil, err + } + return movie, nil + +} + +func FindDegreesOfSeparation(firstActorName string, secondActorName string) (*queue, error) { + + // used to keep track of URLs we've already visited, so we don't visit them again + visitedURLs := make(map[string]bool) + startQueue := queue{URL: firstActorName, degree: 0, path: []string{firstActorName}} + BFSQueue := make([]*queue, 0, 1) + BFSQueue = append(BFSQueue, &startQueue) + var mu sync.Mutex + var wg sync.WaitGroup + + URLCategory := CategoryPerson + + totalWorkers := runtime.NumCPU() + fmt.Println("Total workers: ", totalWorkers) + workerPool := make(chan struct{}, totalWorkers) + + for len(BFSQueue) > 0 { + length := len(BFSQueue) + for i := 0; i < length; i++ { + URL := BFSQueue[i].URL + degree := BFSQueue[i].degree + path := BFSQueue[i].path + + if URLCategory == CategoryPerson {degree = degree + 1} + + if URL == secondActorName { + // fmt.Println("Found the path!") + BFSQueue[i].degree = degree + return BFSQueue[i], nil + } + + workerPool <- struct{}{} // acquire a worker pool to control the number of concurrent workers + wg.Add(1) + + go func(url string) { + defer func() { + <-workerPool // release the worker pool + wg.Done() + }() + if response, err := fetchAPIData(url, URLCategory); err == nil { + // fmt.Println("Response: ", response) + // os.Exit(1) + mu.Lock() + visitedURLs[URL] = true + mu.Unlock() + urlCategory := response.urlType() + // fmt.Println("URL category: ", urlCategory) + if urlCategory == "Person" { + resp := response.(Person) + for _, person := range resp.Movies { + path := make([]string, len(path)) + copy(path, path) + mu.Lock() + if !visitedURLs[person.URL] { + URL = person.URL + path = append(path, person.URL) + q := queue{ + URL: URL, + degree: degree, + path: path, + } + BFSQueue = append(BFSQueue, &q) + } + mu.Unlock() + } + } else if urlCategory == "Movie" { + resp := response.(Movie) + for _, movie := range resp.Cast { + path := make([]string, len(path)) + copy(path, path) + mu.Lock() + if !visitedURLs[movie.URL] { + URL = movie.URL + path = append(path, movie.URL) + q := queue{ + URL: URL, + degree: degree, + path: path, + } + BFSQueue = append(BFSQueue, &q) + } + mu.Unlock() + } + } + } + } (URL) + } + wg.Wait() + URLCategory = 1 - URLCategory + BFSQueue = BFSQueue[length:] + } + return nil, fmt.Errorf("No path found") +} + +func DisplayResult(firstActorName string, secondActorName string) { + // fmt.Println("Displaying result") + result, err := FindDegreesOfSeparation(firstActorName, secondActorName) + if err != nil { + fmt.Println("Error:", err) + return + } + + fmt.Println("Degrees of separation: ", result.degree) + fmt.Println("Path: ", strings.Join(result.path, " -> ")) +} \ No newline at end of file diff --git a/util/utils.go b/util/utils.go new file mode 100644 index 0000000..c546f6a --- /dev/null +++ b/util/utils.go @@ -0,0 +1,9 @@ +package util + +import "regexp" + +var validURLPattern = regexp.MustCompile(`^[a-z]+(?:-[a-z]+)*$`) + +func IsValidURL(url string) bool { + return validURLPattern.MatchString(url) +} \ No newline at end of file