From 863346eac7086a346625dfaf4c461326b2da7cf4 Mon Sep 17 00:00:00 2001 From: Mrinal Wadhwa Date: Wed, 21 Nov 2018 12:19:33 -0800 Subject: [PATCH] feat: add did query support 1. add query to grammar 2. add query support to Parse() 3. add query support to IsReference() 4. add query support tp String() 5. add tests 6. add query examples 7. update README For #5 --- README.md | 25 +++++++--- benchmark_test.go | 16 +++++++ did.abnf | 6 ++- did.go | 114 +++++++++++++++++++++++++++++++++++++++++----- did_test.go | 81 ++++++++++++++++++++++++++++++-- example_test.go | 15 ++++++ 6 files changed, 235 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e0c49f7..173481f 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ following value of DID type. IDStrings:[]string{"q7ckgxeq1lxmra0r"}, Path:"", PathSegments:[]string(nil), + Query:"", Fragment:"" } ``` @@ -66,10 +67,20 @@ which would result in: IDStrings:[]string{"q7ckgxeq1lxmra0r"}, Path:"abc/pqr", PathSegments:[]string{"abc", "pqr"}, + Query:"", Fragment:"" } ``` +or a [DID Reference](https://w3c-ccg.github.io/did-spec/#dfn-did-reference) with a +[DID Path](https://w3c-ccg.github.io/did-spec/#dfn-did-path) and a DID Query: + +```go +d, err := did.Parse("did:example:q7ckgxeq1lxmra0r/abc/pqr?xyz") +fmt.Println(d.Query) +// Output: xyz +``` + or a [DID Reference](https://w3c-ccg.github.io/did-spec/#dfn-did-reference) with a [DID Fragment](https://w3c-ccg.github.io/did-spec/#dfn-did-fragment): @@ -131,17 +142,19 @@ go test -bench=. `did.Parse` included in this package: ``` -BenchmarkParse-8 5000000 345 ns/op -BenchmarkParseWithPath-8 3000000 477 ns/op -BenchmarkParseWithFragment-8 3000000 542 ns/op +BenchmarkParse-8 5000000 365 ns/op +BenchmarkParseWithPath-8 3000000 500 ns/op +BenchmarkParseWithQuery-8 3000000 558 ns/op +BenchmarkParseWithFragment-8 3000000 552 ns/op ``` Go's `url.Parse`: ``` -BenchmarkUrlParse-8 3000000 574 ns/op -BenchmarkUrlParseWithPath-8 3000000 514 ns/op -BenchmarkUrlParseWithFragment-8 5000000 382 ns/op +BenchmarkUrlParse-8 3000000 475 ns/op +BenchmarkUrlParseWithPath-8 3000000 505 ns/op +BenchmarkUrlParseWithQuery-8 5000000 294 ns/op +BenchmarkUrlParseWithFragment-8 5000000 369 ns/op ``` ## Contributing diff --git a/benchmark_test.go b/benchmark_test.go index 2b322dc..8d44fea 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -25,6 +25,14 @@ func BenchmarkParseWithPath(b *testing.B) { parsed = p } +func BenchmarkParseWithQuery(b *testing.B) { + var p *did.DID + for n := 0; n < b.N; n++ { + p, _ = did.Parse("did:ockam:amzbjdl8etgpgwoe841sfi6fc4q9yh82?6pkmkw5pteabvtzm7p6qe106ysiawmo") + } + parsed = p +} + func BenchmarkParseWithFragment(b *testing.B) { var p *did.DID for n := 0; n < b.N; n++ { @@ -53,6 +61,14 @@ func BenchmarkUrlParseWithPath(b *testing.B) { parsedURL = u } +func BenchmarkUrlParseWithQuery(b *testing.B) { + var u *url.URL + for n := 0; n < b.N; n++ { + u, _ = url.Parse("http://amzbjdl8etgpgwoe841sfi6fc4q9yh82.com?6pkmkw5pteabvtzm7p6qe106ysiawm") + } + parsedURL = u +} + func BenchmarkUrlParseWithFragment(b *testing.B) { var u *url.URL for n := 0; n < b.N; n++ { diff --git a/did.abnf b/did.abnf index f085a07..52567ec 100644 --- a/did.abnf +++ b/did.abnf @@ -5,7 +5,7 @@ ; URI Spec: https://tools.ietf.org/html/rfc3986 ; ABNF Spec: https://tools.ietf.org/html/rfc5234 -did-reference = did [ "/" did-path ] [ "#" did-fragment ] +did-reference = did [ "/" did-path ] [ "?" did-query ] [ "#" did-fragment ] did = "did:" method ":" specific-idstring @@ -23,6 +23,10 @@ did-path = segment-nz *( "/" segment ) ; https://tools.ietf.org/html/rfc3986#section-3.5 did-fragment = *( pchar / "/" / "?" ) +; did-query is identical to a URI query and MUST conform to the ABNF of the query ABNF rule in [RFC3986] +; https://tools.ietf.org/html/rfc3986#section-3.4 +did-query = *( pchar / "/" / "?" ) + segment = *pchar segment-nz = 1*pchar pchar = unreserved / pct-encoded / sub-delims / ":" / "@" diff --git a/did.go b/did.go index 3249bee..b87070d 100644 --- a/did.go +++ b/did.go @@ -29,6 +29,11 @@ type DID struct { // did-path = segment-nz *( "/" segment ) PathSegments []string + // DID Query + // https://github.com/w3c-ccg/did-spec/issues/85 + // did-query = *( pchar / "/" / "?" ) + Query string + // DID Fragment, the portion of a DID reference that follows the first hash sign character ("#") // https://w3c-ccg.github.io/did-spec/#dfn-did-fragment Fragment string @@ -45,10 +50,10 @@ type parser struct { // a step in the parser state machine that returns the next step type parserStep func() parserStep -// IsReference returns true if a DID has a Path or a Fragment +// IsReference returns true if a DID has a Path, a Query or a Fragment // https://w3c-ccg.github.io/did-spec/#dfn-did-reference func (d *DID) IsReference() bool { - return (d.Path != "" || len(d.PathSegments) > 0 || d.Fragment != "") + return (d.Path != "" || len(d.PathSegments) > 0 || d.Query != "" || d.Fragment != "") } // String encodes a DID struct into a valid DID string. @@ -85,12 +90,18 @@ func (d *DID) String() string { // write a leading / and then PathSegments joined with / between them buf.WriteByte('/') // nolint, returned error is always nil buf.WriteString(strings.Join(d.PathSegments[:], "/")) // nolint, returned error is always nil - } else { + } + + if d.Query != "" { + // write a leading ? and then Query + buf.WriteByte('?') // nolint, returned error is always nil + buf.WriteString(d.Query) // nolint, returned error is always nil + } + + if d.Fragment != "" && d.Path == "" && len(d.PathSegments) == 0 { // add fragment only when there is no path - if d.Fragment != "" { - buf.WriteByte('#') // nolint, returned error is always nil - buf.WriteString(d.Fragment) // nolint, returned error is always nil - } + buf.WriteByte('#') // nolint, returned error is always nil + buf.WriteString(d.Fragment) // nolint, returned error is always nil } return buf.String() @@ -246,6 +257,12 @@ func (p *parser) parseID() parserStep { break } + if char == '?' { + // encountered ? input may have a query following specific-idstring, parse that next + next = p.parseQuery + break + } + if char == '#' { // encountered # input may have a fragment following specific-idstring, parse that next next = p.parseFragment @@ -312,6 +329,12 @@ func (p *parser) parsePath() parserStep { break } + if char == '?' { + // encountered ? input may have a query following path, parse that next + next = p.parseQuery + break + } + if char == '%' { // a % must be followed by 2 hex digits if (currentIndex+2 >= inputLength) || @@ -352,6 +375,73 @@ func (p *parser) parsePath() parserStep { return next } +// parseQuery is a parserStep that extracts a DID Query from a DID Reference +// from the grammar: +// did-query = *( pchar / "/" / "?" ) +// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +// pct-encoded = "%" HEXDIG HEXDIG +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" +func (p *parser) parseQuery() parserStep { + input := p.input + inputLength := len(input) + currentIndex := p.currentIndex + 1 + startIndex := currentIndex + + var indexIncrement int + var next parserStep + var percentEncoded bool + + for { + if currentIndex == inputLength { + // we've reached the end of input + // it's ok for query to be empty, so we don't need a check for that + // did-query = *( pchar / "/" / "?" ) + break + } + + char := input[currentIndex] + + if char == '#' { + // encountered # input may have a fragment following the query, parse that next + next = p.parseFragment + break + } + + if char == '%' { + // a % must be followed by 2 hex digits + if (currentIndex+2 >= inputLength) || + isNotHexDigit(input[currentIndex+1]) || + isNotHexDigit(input[currentIndex+2]) { + return p.errorf(currentIndex, "%% is not followed by 2 hex digits") + } + // if we got here, we're dealing with percent encoded char, jump three chars + percentEncoded = true + indexIncrement = 3 + } else { + // not pecent encoded + percentEncoded = false + indexIncrement = 1 + } + + // did-query = *( pchar / "/" / "?" ) + // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + // isNotValidQueryOrFragmentChar checks for all the valid chars except pct-encoded + if !percentEncoded && isNotValidQueryOrFragmentChar(char) { + return p.errorf(currentIndex, "character is not allowed in query - %c", char) + } + + // move to the next char + currentIndex = currentIndex + indexIncrement + } + + // update parser state + p.currentIndex = currentIndex + p.out.Query = input[startIndex:currentIndex] + + return next +} + // parseFragment is a parserStep that extracts a DID Fragment from a DID Reference // from the grammar: // did-fragment = *( pchar / "/" / "?" ) @@ -396,9 +486,9 @@ func (p *parser) parseFragment() parserStep { // did-fragment = *( pchar / "/" / "?" ) // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - // isNotValidFragmentChar checks for all othe valid chars except pct-encoded - if !percentEncoded && isNotValidFragmentChar(char) { - return p.errorf(currentIndex, "character is not allowed in fragment") + // isNotValidQueryOrFragmentChar checks for all the valid chars except pct-encoded + if !percentEncoded && isNotValidQueryOrFragmentChar(char) { + return p.errorf(currentIndex, "character is not allowed in fragment - %c", char) } // move to the next char @@ -434,12 +524,12 @@ func isNotValidIDChar(char byte) bool { return isNotAlpha(char) && isNotDigit(char) && char != '.' && char != '-' } -// isNotValidFragmentChar returns true if a byte is not allowed in a Fragment +// isNotValidQueryOrFragmentChar returns true if a byte is not allowed in a Fragment // from the grammar: // did-fragment = *( pchar / "/" / "?" ) // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // pct-encoded is not checked in this function -func isNotValidFragmentChar(char byte) bool { +func isNotValidQueryOrFragmentChar(char byte) bool { return isNotValidPathChar(char) && char != '/' && char != '?' } diff --git a/did_test.go b/did_test.go index a3c654f..21ecbad 100644 --- a/did_test.go +++ b/did_test.go @@ -24,6 +24,11 @@ func TestIsReference(t *testing.T) { assert(t, true, d.IsReference()) }) + t.Run("returns true if Query", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Query: "abc"} + assert(t, true, d.IsReference()) + }) + t.Run("returns true if Fragment", func(t *testing.T) { d := &DID{Method: "example", ID: "123", Fragment: "00000"} assert(t, true, d.IsReference()) @@ -66,6 +71,26 @@ func TestString(t *testing.T) { assert(t, "did:example:123/a/b", d.String()) }) + t.Run("includes Query after IDString", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Query: "abc"} + assert(t, "did:example:123?abc", d.String()) + }) + + t.Run("includes Query after Path", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Path: "x/y", Query: "abc"} + assert(t, "did:example:123/x/y?abc", d.String()) + }) + + t.Run("includes Query after before Fragment", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Fragment: "zyx", Query: "abc"} + assert(t, "did:example:123?abc#zyx", d.String()) + }) + + t.Run("includes Query", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Query: "abc"} + assert(t, "did:example:123?abc", d.String()) + }) + t.Run("includes Fragment", func(t *testing.T) { d := &DID{Method: "example", ID: "123", Fragment: "00000"} assert(t, "did:example:123#00000", d.String()) @@ -239,6 +264,56 @@ func TestParse(t *testing.T) { assert(t, nil, err) }) + t.Run("succeeds to extract query after idstring", func(t *testing.T) { + d, err := Parse("did:a:123?abc") + assert(t, nil, err) + assert(t, "a", d.Method) + assert(t, "123", d.ID) + assert(t, "abc", d.Query) + }) + + t.Run("succeeds to extract query after path", func(t *testing.T) { + d, err := Parse("did:a:123/a/b/c?abc") + assert(t, nil, err) + assert(t, "a", d.Method) + assert(t, "123", d.ID) + assert(t, "a/b/c", d.Path) + assert(t, "abc", d.Query) + }) + + t.Run("succeeds to extract fragment after query", func(t *testing.T) { + d, err := Parse("did:a:123?abc#xyz") + assert(t, nil, err) + assert(t, "abc", d.Query) + assert(t, "xyz", d.Fragment) + }) + + t.Run("succeeds with percent encoded chars in query", func(t *testing.T) { + d, err := Parse("did:a:123?ab%20c") + assert(t, nil, err) + assert(t, "ab%20c", d.Query) + }) + + t.Run("returns error if % in query is not followed by 2 hex chars", func(t *testing.T) { + dids := []string{ + "did:a:123:456?%", + "did:a:123:456?%a", + "did:a:123:456?%!*", + "did:a:123:456?%A!", + "did:xyz:pqr?%A!", + "did:a:123:456?%A%", + } + for _, did := range dids { + _, err := Parse(did) + assert(t, false, err == nil, "Input: %s", did) + } + }) + + t.Run("returns error if query has invalid char", func(t *testing.T) { + _, err := Parse("did:a:123:456?ssss^sss") + assert(t, false, err == nil) + }) + t.Run("succeeds to extract fragment", func(t *testing.T) { d, err := Parse("did:a:123:456#keys-1") assert(t, nil, err) @@ -303,7 +378,7 @@ func Test_isNotValidIDChar(t *testing.T) { } } -func Test_isNotValidFragmentChar(t *testing.T) { +func Test_isNotValidQueryOrFragmentChar(t *testing.T) { a := []byte{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', @@ -313,12 +388,12 @@ func Test_isNotValidFragmentChar(t *testing.T) { ':', '@', '/', '?'} for _, c := range a { - assert(t, false, isNotValidFragmentChar(c), "Input: '%c'", c) + assert(t, false, isNotValidQueryOrFragmentChar(c), "Input: '%c'", c) } a = []byte{'%', '^', '#', ' '} for _, c := range a { - assert(t, true, isNotValidFragmentChar(c), "Input: '%c'", c) + assert(t, true, isNotValidQueryOrFragmentChar(c), "Input: '%c'", c) } } diff --git a/example_test.go b/example_test.go index b041752..783fc0f 100644 --- a/example_test.go +++ b/example_test.go @@ -25,6 +25,15 @@ func ExampleParse_withPath() { // Output: Method - example, ID - q7ckgxeq1lxmra0r, Path - a/b } +func ExampleParse_withQuery() { + d, err := did.Parse("did:example:q7ckgxeq1lxmra0r?dskjsdjj") + if err != nil { + log.Fatal(err) + } + fmt.Printf("Method - %s, ID - %s, Query - %s", d.Method, d.ID, d.Query) + // Output: Method - example, ID - q7ckgxeq1lxmra0r, Query - dskjsdjj +} + func ExampleParse_withFragment() { d, err := did.Parse("did:example:q7ckgxeq1lxmra0r#keys-1") if err != nil { @@ -52,6 +61,12 @@ func ExampleDID_String_withPathSegments() { // Output: did:example:q7ckgxeq1lxmra0r/a/b } +func ExampleDID_String_withQuery() { + d := &did.DID{Method: "example", ID: "q7ckgxeq1lxmra0r", Query: "abc"} + fmt.Println(d.String()) + // Output: did:example:q7ckgxeq1lxmra0r?abc +} + func ExampleDID_String_withFragment() { d := &did.DID{Method: "example", ID: "q7ckgxeq1lxmra0r", Fragment: "keys-1"} fmt.Println(d.String())