diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..456d79e --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +GOPATH=$(shell go env GOPATH) + +.PHONY: test +test: + @echo "==> Running tests" + go test -v ./... diff --git a/README.md b/README.md index e69de29..198158d 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,7 @@ +# lot-sh/core + +## Testing + +```shell +make test +``` diff --git a/resolver_factory.go b/resolver_factory.go index 4297ba7..14fefa2 100644 --- a/resolver_factory.go +++ b/resolver_factory.go @@ -1,18 +1,22 @@ package core -import "errors" +import ( + "errors" + + "github.com/lot-sh/core/scheme" +) // ResolverFactory returns the implementation of Resolver -// which can handle the SchemeType passed as argument +// which can handle the scheme.Type passed as argument // -// • when there is not a Resolver associated to the SchemeType +// • when there is not a Resolver associated to the scheme.Type // passed as argument it will return a nil Resolver and an error -func ResolverFactory(st SchemeType) (Resolver, error) { +func ResolverFactory(st scheme.Type) (Resolver, error) { var resolver Resolver switch st { - case HTTP, HTTPS: + case scheme.HTTP, scheme.HTTPS: resolver = &HTTPResolver{} return resolver, nil } - return resolver, errors.New("There is not implementation known which supports the given SchemeType") + return resolver, errors.New("There is not implementation known which supports the given scheme.Type") } diff --git a/resolver_factory_test.go b/resolver_factory_test.go index 4088556..dd06766 100644 --- a/resolver_factory_test.go +++ b/resolver_factory_test.go @@ -1,17 +1,20 @@ package core -import "testing" +import ( + "testing" + "github.com/lot-sh/core/scheme" +) -func TestResolverFactoryShouldWorksWhenPassedHTTPSchemeType(t *testing.T) { - _, err := ResolverFactory(HTTP) +func TestResolverFactoryShouldWorksWhenPassedHTTPType(t *testing.T) { + _, err := ResolverFactory(scheme.HTTP) if err != nil { - t.Error("Error should be no returned by ResolverFactory when passed a HTTP SchemeType") + t.Error("Error should be no returned by ResolverFactory when passed a HTTP Type") } } -func TestResolverFactoryShouldReturnErrorWhenPassedUNKNOWNSchemeType(t *testing.T) { - _, err := ResolverFactory(UNKNOWN) +func TestResolverFactoryShouldReturnErrorWhenPassedUNKNOWNType(t *testing.T) { + _, err := ResolverFactory(scheme.UNKNOWN) if err == nil { - t.Error("Error should be returned by ResolverFactory when passed a UNKNONW SchemeType") + t.Error("Error should be returned by ResolverFactory when passed a UNKNONW Type") } } diff --git a/resource.go b/resource.go index 43cfd4c..7226810 100644 --- a/resource.go +++ b/resource.go @@ -3,13 +3,14 @@ package core import ( "fmt" "io" + "github.com/lot-sh/core/scheme" ) // Resource struct is the data of the principal resource of // this application, which is pieces of code and they origin type Resource struct { Locator string - Scheme SchemeType + Scheme scheme.Type } func (r *Resource) String() string { diff --git a/resource/locator.go b/resource/locator.go new file mode 100644 index 0000000..62966cd --- /dev/null +++ b/resource/locator.go @@ -0,0 +1,67 @@ +package resource + +import ( + "fmt" + "strings" + + "github.com/lot-sh/core/scheme" +) + +// Locator represents the minimal locator functionality +// For more details visit the RFC +// https://github.com/lot-sh/docs/blob/master/rfcs/002-resource-locator.md +type Locator struct { + scheme scheme.Type + path string +} + +// NewLocator instance a locator from a given string which parse +// and ensure if the string is well formated +func NewLocator(strloc string) (*Locator, error) { + res := Locator{} + parts := strings.Split(strloc, scheme.SEPARATOR) + res.scheme = scheme.GetTypeFrom(parts[0]) + + if res.scheme == scheme.UNKNOWN { + return nil, fmt.Errorf("Failure schema detection when parsing %s", strloc) + } + + partsLen := len(parts) + if partsLen != 2 { + if partsLen == 1 { + return nil, fmt.Errorf("Malformatted locator missing path when parsing %s", strloc) + } + + return nil, fmt.Errorf("Malformatted locator when parsing %s", strloc) + } + + res.path = parts[1] + + return &res, nil +} + +// Scheme gets the scheme type with string format +func (l *Locator) Scheme() string { + return l.scheme.String() +} + +// Path returns the part of the locator wich contains +// the identification of a resource under the specified scheme +func (l *Locator) Path() string { + return l.path +} + +// Tag returns the name of version of a resource +func (l *Locator) Tag() string { + parts := strings.Split(l.path, "@") + if len(parts) > 1 { + return parts[1] + } + return "" +} + +// String format representation +func (l *Locator) String() string { + s := []string{l.Scheme(), l.Path()} + return strings.Join(s, scheme.SEPARATOR) +} diff --git a/resource/locator_test.go b/resource/locator_test.go new file mode 100644 index 0000000..2d94fb0 --- /dev/null +++ b/resource/locator_test.go @@ -0,0 +1,142 @@ +package resource + +import ( + "fmt" + "strings" + "testing" +) + +var suitePassMsg = ` +PASS: %s +-> Input: %s` + +var suiteFailMsg = ` +FAIL: %s +-> Input: %s +-> Actual %s +-> Expected: %s` + +func TestLocatorShouldParseAndFormatWithoutErrors(t *testing.T) { + type suiteExpected struct { + scheme string + path string + tag string + } + suites := []struct { + message string + input string + expected suiteExpected + }{ + { + "Regular http resource", + "http://example.com/testfile", + suiteExpected{ + "http", "//example.com/testfile", "", + }, + }, { + "Regular ftp resource", + "ftp://example.com/testfile?test#foo", + suiteExpected{ + "ftp", "//example.com/testfile?test#foo", "", + }, + }, { + "With tag", + "lot:rubeniskov/semver-patcher@v1.0.1", + suiteExpected{ + "lot", "rubeniskov/semver-patcher@v1.0.1", "v1.0.1", + }, + }, { + "With ID", + "lot:QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX", + suiteExpected{ + "lot", "QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX", "", + }, + }, + } + + for _, suite := range suites { + loc, err := NewLocator(suite.input) + if err != nil { + t.Error(err) + } + if loc.Scheme() != suite.expected.scheme { + t.Errorf( + suiteFailMsg, + fmt.Sprintf("%s: Wrong expected scheme", suite.message), + suite.input, + loc.Scheme(), + suite.expected.scheme, + ) + } + if loc.Path() != suite.expected.path { + t.Errorf( + suiteFailMsg, + fmt.Sprintf("%s: Wrong expected path", suite.message), + loc.Path(), + suite.expected.path, + ) + } + if loc.Tag() != suite.expected.tag { + t.Errorf( + suiteFailMsg, + fmt.Sprintf("%s: Wrong expected tag", suite.message), + loc.Path(), + suite.expected.tag, + ) + } + t.Logf( + suitePassMsg, + fmt.Sprintf( + "%s should parse without errors (%s)", + suite.message, + loc, + ), + suite.input, + ) + } +} + +func TestLocatorShouldRaiseErrorScheme(t *testing.T) { + suites := []struct { + message string + input string + expected string + }{ + { + "Missing scheme", + "//example.com/testfile", + "Failure schema detection when parsing //example.com/testfile", + }, { + "Wrong scheme", + "scheme:", + "Failure schema detection when parsing scheme:", + }, { + "Missing path", + "ftp", + "Malformatted locator missing path when parsing ftp", + }, { + "Wrong path", + "ftp:test:", + "Malformatted locator when parsing ftp:test:", + }, + } + + for _, suite := range suites { + _, err := NewLocator(suite.input) + if err == nil { + t.Error(err) + } + if !strings.Contains(err.Error(), suite.expected) { + t.Errorf(suiteFailMsg, suite.message, suite.input, err.Error(), suite.expected) + } + t.Logf( + suitePassMsg, + fmt.Sprintf( + "%s should raise error with the espected message (%s)", + suite.message, + err.Error(), + ), + suite.input, + ) + } +} diff --git a/resource_factory.go b/resource_factory.go index 061bec5..fdf243d 100644 --- a/resource_factory.go +++ b/resource_factory.go @@ -1,16 +1,20 @@ package core -import "errors" +import ( + "errors" + + "github.com/lot-sh/core/scheme" +) // ResourceFactory function returns a Resource given a locator func ResourceFactory(locator string) (Resource, error) { - var scheme SchemeType = GetSchemeTypeFrom(locator) + var sch scheme.Type = scheme.GetTypeFrom(locator) var resource Resource = Resource{ locator, - scheme, + sch, } - if scheme == UNKNOWN { - return resource, errors.New("Unknown scheme, the locator may be a invalid one") + if sch == scheme.UNKNOWN { + return resource, errors.New("Unknown scheme.Type, the locator may be a invalid one") } return resource, nil } diff --git a/resource_factory_test.go b/resource_factory_test.go index bb871cd..ffa29ec 100644 --- a/resource_factory_test.go +++ b/resource_factory_test.go @@ -1,6 +1,9 @@ package core -import "testing" +import ( + "testing" + "github.com/lot-sh/core/scheme" +) func TestResourceFactoryShouldWorkAsExpected(t *testing.T) { locator := "lot:QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX" @@ -8,8 +11,8 @@ func TestResourceFactoryShouldWorkAsExpected(t *testing.T) { if err != nil { t.Error(err) } - if resource.Scheme != LOT { - t.Errorf("the scheme of %s should be %s, found %s", locator, LOT, resource.Scheme) + if resource.Scheme != scheme.LOT { + t.Errorf("the scheme of %s should be %s, found %s", locator, scheme.LOT, resource.Scheme) } if resource.Locator != locator { t.Errorf("the locator should be %s, found %s", locator, resource.Locator) diff --git a/scheme/scheme_type.go b/scheme/scheme_type.go new file mode 100644 index 0000000..092fc31 --- /dev/null +++ b/scheme/scheme_type.go @@ -0,0 +1,63 @@ +package scheme + +import "regexp" + +// Type type +type Type int + +// Scheme types supported by this program. Also +// UNKNOWN is defined to be used when the scheme is unknown +// https://tools.ietf.org/html/rfc3986#section-3 +const ( + FTP Type = iota + GIST + HTTP + HTTPS + IPFS + LOT + UNKNOWN +) + +// SEPARATOR defines where the scheme definition +// ends in a formated string +const SEPARATOR = ":" + +// ListNames string naming mapping for each Type +var ListNames = []string{ + "ftp", + "gist", + "http", + "https", + "ipfs", + "lot", +} + +func (st Type) String() string { + if st < 0 || st >= Type(len(ListNames)) { + return "unknown" + } + return ListNames[st] +} + +// GetTypeFrom try to identify the scheme of the locator +// and returns the Type which correspond, can return the +// Type.UNKNOWN in case it cannot identify the scheme +func GetTypeFrom(locator string) Type { + re := regexp.MustCompile(`^[a-z]*`) + switch string(re.Find([]byte(locator))) { + case "ftp": + return FTP + case "gist": + return GIST + case "http": + return HTTP + case "https": + return HTTPS + case "ipfs": + return IPFS + case "lot": + return LOT + default: + return UNKNOWN + } +} diff --git a/scheme/scheme_type_test.go b/scheme/scheme_type_test.go new file mode 100644 index 0000000..e5197bd --- /dev/null +++ b/scheme/scheme_type_test.go @@ -0,0 +1,47 @@ +package scheme + +import "testing" + +func TestGetTypeFromShouldWorkAsExpected(t *testing.T) { + locatorFTP := "ftp://username:pass@example.com" + if GetTypeFrom(locatorFTP) != FTP { + t.Errorf("The Type of %s should be FTP, instead got %s", locatorFTP, GetTypeFrom(locatorFTP)) + } + locatorGist := "gist:username/path/to/file.sh" + if GetTypeFrom(locatorGist) != GIST { + t.Errorf("The Type of %s should be FTP, instead got %s", locatorGist, GetTypeFrom(locatorGist)) + } + locatorHTTP := "http://thelifeofbrian.com/the/best/film.sh" + if GetTypeFrom(locatorHTTP) != HTTP { + t.Errorf("The Type of %s should be FTP, instead got %s", locatorHTTP, GetTypeFrom(locatorHTTP)) + } + locatorHTTPS := "https://gist.githubusercontent.com/Kelvur/896265085d32db2c3ab065ea0995b0a3/raw/62df0ad25eed20cdfc27235e8c39cbbbdf967ed3/2020_content.md" + if GetTypeFrom(locatorHTTPS) != HTTPS { + t.Errorf("The Type of %s should be FTP, instead got %s", locatorHTTPS, GetTypeFrom(locatorHTTPS)) + } + locatorIPFS := "ipfs:QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX" + if GetTypeFrom(locatorIPFS) != IPFS { + t.Errorf("The Type of %s should be FTP, instead got %s", locatorIPFS, GetTypeFrom(locatorIPFS)) + } + locatorLot := "lot:QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX" + if GetTypeFrom(locatorLot) != LOT { + t.Errorf("The Type of %s should be FTP, instead got %s", locatorLot, GetTypeFrom(locatorLot)) + } + locatorUnknown := "monty://john.cleese/dead/parrot" + if GetTypeFrom(locatorUnknown) != UNKNOWN { + t.Errorf("The Type of %s should be FTP, instead got %s", locatorUnknown, GetTypeFrom(locatorUnknown)) + } +} + +func TestCastingString(t *testing.T) { + actual := FTP.String() + expected := "ftp" + if actual != "ftp" { + t.Errorf("The Type %s should be casted as string with the value of %s, instead got %s", FTP, expected, actual) + } + actual = HTTP.String() + expected = "http" + if actual != "http" { + t.Errorf("The Type %s should be casted as string with the value of %s, instead got %s", HTTP, expected, actual) + } +} diff --git a/scheme_type.go b/scheme_type.go deleted file mode 100644 index 7af067e..0000000 --- a/scheme_type.go +++ /dev/null @@ -1,57 +0,0 @@ -package core - -import "regexp" - -// SchemeType type -type SchemeType int - -// Scheme types supported by this program. Also -// UNKNOWN is defined to be used when the scheme is unknown -// https://tools.ietf.org/html/rfc3986#section-3 -const ( - FTP SchemeType = iota - GIST - HTTP - HTTPS - IPFS - LOT - UNKNOWN -) - -func (st SchemeType) String() string { - listNames := []string{ - "ftp", - "gist", - "http", - "https", - "ipfs", - "lot", - } - if st < 0 || st >= SchemeType(len(listNames)) { - return "unknown" - } - return listNames[st] -} - -// GetSchemeTypeFrom try to identify the scheme of the locator -// and returns the SchemeType which correspond, can return the -// SchemeType.UNKNOWN in case it cannot identify the scheme -func GetSchemeTypeFrom(locator string) SchemeType { - re := regexp.MustCompile(`^[a-z]*`) - switch string(re.Find([]byte(locator))) { - case "ftp": - return FTP - case "gist": - return GIST - case "http": - return HTTP - case "https": - return HTTPS - case "ipfs": - return IPFS - case "lot": - return LOT - default: - return UNKNOWN - } -} diff --git a/scheme_type_test.go b/scheme_type_test.go deleted file mode 100644 index e23efaf..0000000 --- a/scheme_type_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package core - -import "testing" - -func TestGetSchemeTypeFromShouldWorkAsExpected(t *testing.T) { - locatorFTP := "ftp://username:pass@example.com" - if GetSchemeTypeFrom(locatorFTP) != FTP { - t.Errorf("The SchemeType of %s should be FTP, instead got %s", locatorFTP, GetSchemeTypeFrom(locatorFTP)) - } - locatorGist := "gist:username/path/to/file.sh" - if GetSchemeTypeFrom(locatorGist) != GIST { - t.Errorf("The SchemeType of %s should be FTP, instead got %s", locatorGist, GetSchemeTypeFrom(locatorGist)) - } - locatorHTTP := "http://thelifeofbrian.com/the/best/film.sh" - if GetSchemeTypeFrom(locatorHTTP) != HTTP { - t.Errorf("The SchemeType of %s should be FTP, instead got %s", locatorHTTP, GetSchemeTypeFrom(locatorHTTP)) - } - locatorHTTPS := "https://gist.githubusercontent.com/Kelvur/896265085d32db2c3ab065ea0995b0a3/raw/62df0ad25eed20cdfc27235e8c39cbbbdf967ed3/2020_content.md" - if GetSchemeTypeFrom(locatorHTTPS) != HTTPS { - t.Errorf("The SchemeType of %s should be FTP, instead got %s", locatorHTTPS, GetSchemeTypeFrom(locatorHTTPS)) - } - locatorIPFS := "ipfs:QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX" - if GetSchemeTypeFrom(locatorIPFS) != IPFS { - t.Errorf("The SchemeType of %s should be FTP, instead got %s", locatorIPFS, GetSchemeTypeFrom(locatorIPFS)) - } - locatorLot := "lot:QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX" - if GetSchemeTypeFrom(locatorLot) != LOT { - t.Errorf("The SchemeType of %s should be FTP, instead got %s", locatorLot, GetSchemeTypeFrom(locatorLot)) - } - locatorUnknown := "monty://john.cleese/dead/parrot" - if GetSchemeTypeFrom(locatorUnknown) != UNKNOWN { - t.Errorf("The SchemeType of %s should be FTP, instead got %s", locatorUnknown, GetSchemeTypeFrom(locatorUnknown)) - } -}