Skip to content

Commit

Permalink
feat: add did query support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mrinalwadhwa committed Nov 21, 2018
1 parent bdd1431 commit 863346e
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 22 deletions.
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ following value of DID type.
IDStrings:[]string{"q7ckgxeq1lxmra0r"},
Path:"",
PathSegments:[]string(nil),
Query:"",
Fragment:""
}
```
Expand All @@ -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):

Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++ {
Expand Down Expand Up @@ -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++ {
Expand Down
6 changes: 5 additions & 1 deletion did.abnf
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 / ":" / "@"
Expand Down
114 changes: 102 additions & 12 deletions did.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -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 / "/" / "?" )
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 != '?'
}

Expand Down
Loading

0 comments on commit 863346e

Please sign in to comment.