From b46edf935687a1634e6f8b6a79f5a0083c36ff92 Mon Sep 17 00:00:00 2001 From: "Miguel Osorio @Kelvur" Date: Wed, 3 Jun 2020 09:05:27 +0200 Subject: [PATCH 1/4] feat: implement GistResolver --- gist_resolver.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 gist_resolver.go diff --git a/gist_resolver.go b/gist_resolver.go new file mode 100644 index 0000000..72fe2b9 --- /dev/null +++ b/gist_resolver.go @@ -0,0 +1,91 @@ +package core + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "regexp" +) + +// GistResolver is a +// +// • It implements the infertace Resolver +type GistResolver struct { +} + +// GistFileResponse struct +type GistFileResponse struct { + Filename string + Raw_URL string +} + +// GistResponse struct +type GistResponse struct { + ID string + Files map[string]GistFileResponse +} + +// GithubAPIURL constant reference the base url of Github API +var GithubAPIURL string = "https://api.github.com" + +// GistLocatorRegexp regexp to validate and match the useful parts of the gist locator +var GistLocatorRegexp string = `^[a-zA-Z0-9]+://(?P[a-zA-Z0-9_]+[a-zA-Z0-9])/(?P[^/<>|:&]+)$` + +// ObtainUsernameAndResourceFromGistLocator function +func ObtainUsernameAndResourceFromGistLocator(locator string) ([2]string, error) { + re := regexp.MustCompile(GistLocatorRegexp) + if !re.MatchString(locator) { + return [2]string{}, fmt.Errorf("The locator %s doesn't match a well formed gist locator", locator) + } + matchList := re.FindAllStringSubmatch(locator, -1)[0] + return [2]string{matchList[1], matchList[2]}, nil +} + +// GetUserGistURL function +func GetUserGistURL(username string) string { + return fmt.Sprintf("%s/users/%s/gists", GithubAPIURL, username) +} + +// FetchData function +func (resolver *GistResolver) FetchData(resource *Resource) (io.ReadCloser, error) { + values, err := ObtainUsernameAndResourceFromGistLocator(resource.Locator) + if err != nil { + return nil, err + } + url := GetUserGistURL(values[0]) + // Fetch all the gists of the user + res, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("Unable to fetch the url %s %v", url, err) + } + // Defer the close of the gists stream + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + // Decode the response + var gistList []GistResponse + if err = json.Unmarshal(body, &gistList); err != nil { + return nil, fmt.Errorf("Unable to decode the request to %s %v", url, err) + } + // Look for the resource in all the gist + var found GistFileResponse = GistFileResponse{"", ""} + for idxGist := range gistList { + for filename, fileList := range gistList[idxGist].Files { + // Check if the filename is equals to the resource ol the locator + if filename == values[1] { + found = fileList + break + } + } + } + if found.Filename == "" { + return nil, fmt.Errorf("%s not found for the user %s", values[1], values[0]) + } + // Leave the fetch to HTTPResolver + httpResolver := &HTTPResolver{} + return httpResolver.FetchData(&Resource{ + found.Raw_URL, + HTTPS, + }) +} From cd3bc341a6097ee1de663b1c9e0ac926688da791 Mon Sep 17 00:00:00 2001 From: "Miguel Osorio @Kelvur" Date: Wed, 3 Jun 2020 09:05:40 +0200 Subject: [PATCH 2/4] test: GistResolver --- gist_resolver_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 gist_resolver_test.go diff --git a/gist_resolver_test.go b/gist_resolver_test.go new file mode 100644 index 0000000..b9e7702 --- /dev/null +++ b/gist_resolver_test.go @@ -0,0 +1,70 @@ +package core + +import ( + "regexp" + "testing" +) + +func TestGistLocatorRegexpShouldBeWellFormed(t *testing.T) { + re := regexp.MustCompile(GistLocatorRegexp) + locator := "gist://Kelvur/multiply.py" + username := "Kelvur" + resource := "multiply.py" + if !re.MatchString(locator) { + t.Errorf("GistLocatorRegexp doesn't match the locator: %s", locator) + } + listMatches := re.FindAllStringSubmatch("gist://Kelvur/multiply.py", -1)[0] + if listMatches[1] != "Kelvur" { + t.Errorf("GistLocatorRegexp doesn't match correctly the username, expected %s get %s", username, listMatches[1]) + } + if listMatches[2] != "multiply.py" { + t.Errorf("GistLocatorRegexp doesn't match correctly the resource, expected %s get %s", resource, listMatches[2]) + } +} + +func TestObtainUsernameAndResourceFromGistLocatorShouldWorkAsExpected(t *testing.T) { + gistLocator := "gist://Kelvur/multiply.py" + username := "Kelvur" + resource := "multiply.py" + values, err := ObtainUsernameAndResourceFromGistLocator(gistLocator) + if err != nil { + t.Errorf("ObtainUsernameAndResourceFromGistLocator should work with a valid gist locator as %s", gistLocator) + } + if values[0] != username { + t.Errorf("after calling ObtainUsernameAndResourceFromGistLocator with %s the expected fist value is %s, get %s", gistLocator, username, values[0]) + } + if values[1] != resource { + t.Errorf("after calling ObtainUsernameAndResourceFromGistLocator with %s the expected fist value is %s, get %s", gistLocator, resource, values[1]) + } +} + +func TestObtainUsernameAndResourceFromGistLocatorShouldFailAsExpected(t *testing.T) { + incorrectGistLocator := "gist:/-asd/<>" + _, err := ObtainUsernameAndResourceFromGistLocator(incorrectGistLocator) + if err == nil { + t.Errorf("ObtainUsernameAndResourceFromGistLocator should fail when called with %s", incorrectGistLocator) + } +} + +func TestGistResolverImplementsResolverInterface(t *testing.T) { + var resolver Resolver = &GistResolver{} + _, ok := resolver.(*GistResolver) + if !ok { + t.Error("GistResolver not implements Resolver") + } +} + +func TestGistResolverShouldBeAbleToFetchAGist(t *testing.T) { + gistLocator := "gist://Kelvur/multiplication.py" + resource, err := ResourceFactory(gistLocator) + if err != nil { + t.Errorf("ResourceFactory fails with locator %s", gistLocator) + t.Error(err) + } + var resolver GistResolver = GistResolver{} + readCloser, err := resolver.FetchData(&resource) + defer readCloser.Close() + if err != nil { + t.Error(err) + } +} From c55850c849c591e9cee93a580d3e1dfa51519b6b Mon Sep 17 00:00:00 2001 From: "Miguel Osorio @Kelvur" Date: Thu, 4 Jun 2020 18:46:25 +0200 Subject: [PATCH 3/4] docs: improve the comments/documentation of gist resolver --- gist_resolver.go | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/gist_resolver.go b/gist_resolver.go index 72fe2b9..0408d59 100644 --- a/gist_resolver.go +++ b/gist_resolver.go @@ -9,31 +9,34 @@ import ( "regexp" ) -// GistResolver is a +// GistResolver is able to understand gist locators and fetch +// them returning a stream // // • It implements the infertace Resolver type GistResolver struct { } -// GistFileResponse struct -type GistFileResponse struct { +// UserGistFile represents a gist file in a Gisthub response +type UserGistFile struct { Filename string Raw_URL string } -// GistResponse struct -type GistResponse struct { +// UserGistsResponse represents the response of Github when +// fetching all the gist of a user +type UserGistsResponse struct { ID string - Files map[string]GistFileResponse + Files map[string]UserGistFile } // GithubAPIURL constant reference the base url of Github API var GithubAPIURL string = "https://api.github.com" -// GistLocatorRegexp regexp to validate and match the useful parts of the gist locator +// GistLocatorRegexp regexp to validate and match the parts of the gist locator var GistLocatorRegexp string = `^[a-zA-Z0-9]+://(?P[a-zA-Z0-9_]+[a-zA-Z0-9])/(?P[^/<>|:&]+)$` -// ObtainUsernameAndResourceFromGistLocator function +// ObtainUsernameAndResourceFromGistLocator return an array +// with the username and resource of a gist locator func ObtainUsernameAndResourceFromGistLocator(locator string) ([2]string, error) { re := regexp.MustCompile(GistLocatorRegexp) if !re.MatchString(locator) { @@ -43,12 +46,14 @@ func ObtainUsernameAndResourceFromGistLocator(locator string) ([2]string, error) return [2]string{matchList[1], matchList[2]}, nil } -// GetUserGistURL function +// GetUserGistURL builds a URL which points to all the gist of +// a user func GetUserGistURL(username string) string { return fmt.Sprintf("%s/users/%s/gists", GithubAPIURL, username) } -// FetchData function +// FetchData function fetch the code where the resource Locator +// points to, in this case the resource points to a gist func (resolver *GistResolver) FetchData(resource *Resource) (io.ReadCloser, error) { values, err := ObtainUsernameAndResourceFromGistLocator(resource.Locator) if err != nil { @@ -64,12 +69,13 @@ func (resolver *GistResolver) FetchData(resource *Resource) (io.ReadCloser, erro defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) // Decode the response - var gistList []GistResponse + var gistList []UserGistsResponse + if err = json.Unmarshal(body, &gistList); err != nil { return nil, fmt.Errorf("Unable to decode the request to %s %v", url, err) } // Look for the resource in all the gist - var found GistFileResponse = GistFileResponse{"", ""} + var found UserGistFile = UserGistFile{"", ""} for idxGist := range gistList { for filename, fileList := range gistList[idxGist].Files { // Check if the filename is equals to the resource ol the locator From 46bc044d8eb9cb4ba176467e3e346fdffc4279fe Mon Sep 17 00:00:00 2001 From: "Miguel Osorio @Kelvur" Date: Thu, 4 Jun 2020 18:47:06 +0200 Subject: [PATCH 4/4] test: Test if the regexp fails as expected --- gist_resolver_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gist_resolver_test.go b/gist_resolver_test.go index b9e7702..59252d4 100644 --- a/gist_resolver_test.go +++ b/gist_resolver_test.go @@ -22,6 +22,22 @@ func TestGistLocatorRegexpShouldBeWellFormed(t *testing.T) { } } +func TestGistLocatorRegexpShouldFailsToMatchBadFormedGistLocators(t *testing.T) { + re := regexp.MustCompile(GistLocatorRegexp) + locatorWithoutScheme := "Kelvur/multiply.py" + if re.MatchString(locatorWithoutScheme) { + t.Errorf("GistLocatorRegexp match a locator without scheme: %s", locatorWithoutScheme) + } + locatorWithoutUsername := "gist://multiply.py" + if re.MatchString(locatorWithoutUsername) { + t.Errorf("GistLocatorRegexp match a locator without username: %s", locatorWithoutUsername) + } + locatorWithoutResource := "gist://Kelvur/" + if re.MatchString(locatorWithoutResource) { + t.Errorf("GistLocatorRegexp match a locator without resource: %s", locatorWithoutResource) + } +} + func TestObtainUsernameAndResourceFromGistLocatorShouldWorkAsExpected(t *testing.T) { gistLocator := "gist://Kelvur/multiply.py" username := "Kelvur"