generated from okp4/template-oss
-
Notifications
You must be signed in to change notification settings - Fork 128
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #277 from okp4/feat/did-components-predicate
Feat/did components predicate
- Loading branch information
Showing
7 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package predicate | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"strings" | ||
|
||
"github.com/ichiban/prolog/engine" | ||
godid "github.com/nuts-foundation/go-did/did" | ||
"github.com/okp4/okp4d/x/logic/util" | ||
) | ||
|
||
// AtomDID is a term which represents a DID as a compound term `did(Method, ID, Path, Query, Fragment)`. | ||
var AtomDID = engine.NewAtom("did") | ||
|
||
// DIDComponents is a predicate which breaks down a DID into its components according to the [W3C DID] specification. | ||
// | ||
// did_components(+DID, -Components) is det | ||
// did_components(-DID, +Components) is det | ||
// | ||
// where: | ||
// - `DID` represents the DID URI as a `text`, compliant with the [W3C DID] specification. | ||
// - `Components` is a term `did(Method, ID, Path, Query, Fragment)` following the [DID syntax] which represents | ||
// respectively the method name, the method-specific ID, the path, the query, and the fragment of the DID, in decoded | ||
// form. Components that are not found (i.e. `null`) are left uninstantiated (variable). | ||
// | ||
// Example: | ||
// | ||
// # Decompose a DID into its components. | ||
// - did_components('did:example:123456?versionId=1', did(Method, ID, Path, Query, Fragment)). | ||
// | ||
// # Reconstruct a DID from its components. | ||
// - did_components(DID, did('example', '123456', null, 'versionId=1', _42)). | ||
// | ||
// [W3C DID]: https://w3c.github.io/did-core | ||
// [DID syntax]: https://w3c.github.io/did-core/#did-syntax | ||
func DIDComponents(vm *engine.VM, did, components engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { | ||
switch t1 := env.Resolve(did).(type) { | ||
case engine.Variable: | ||
case engine.Atom: | ||
parsedDid, err := godid.ParseDIDURL(t1.String()) | ||
if err != nil { | ||
return engine.Error(fmt.Errorf("did_components/2: %w", err)) | ||
} | ||
|
||
terms, err := didToTerms(parsedDid) | ||
if err != nil { | ||
return engine.Error(fmt.Errorf("did_components/2: %w", err)) | ||
} | ||
|
||
return engine.Unify(vm, components, AtomDID.Apply(terms...), cont, env) | ||
default: | ||
return engine.Error(fmt.Errorf("did_components/2: cannot unify did with %T", t1)) | ||
} | ||
|
||
switch t2 := env.Resolve(components).(type) { | ||
case engine.Variable: | ||
return engine.Error(fmt.Errorf("did_components/2: at least one argument must be instantiated")) | ||
case engine.Compound: | ||
if t2.Functor() != AtomDID { | ||
return engine.Error(fmt.Errorf("did_components/2: invalid functor %s. Expected %s", t2.Functor().String(), AtomDID.String())) | ||
} | ||
if t2.Arity() != 5 { | ||
return engine.Error(fmt.Errorf("did_components/2: invalid arity %d. Expected 5", t2.Arity())) | ||
} | ||
|
||
buf := strings.Builder{} | ||
buf.WriteString("did:") | ||
if segment, ok := util.Resolve(env, t2.Arg(0)); ok { | ||
buf.WriteString(url.PathEscape(segment.String())) | ||
} | ||
if segment, ok := util.Resolve(env, t2.Arg(1)); ok { | ||
buf.WriteString(":") | ||
buf.WriteString(url.PathEscape(segment.String())) | ||
} | ||
if segment, ok := util.Resolve(env, t2.Arg(2)); ok { | ||
for _, s := range strings.FieldsFunc(segment.String(), func(c rune) bool { return c == '/' }) { | ||
buf.WriteString("/") | ||
buf.WriteString(url.PathEscape(s)) | ||
} | ||
} | ||
if segment, ok := util.Resolve(env, t2.Arg(3)); ok { | ||
buf.WriteString("?") | ||
buf.WriteString(url.PathEscape(segment.String())) | ||
} | ||
if segment, ok := util.Resolve(env, t2.Arg(4)); ok { | ||
buf.WriteString("#") | ||
buf.WriteString(url.PathEscape(segment.String())) | ||
} | ||
return engine.Unify(vm, did, engine.NewAtom(buf.String()), cont, env) | ||
default: | ||
return engine.Error(fmt.Errorf("did_components/2: cannot unify did with %T", t2)) | ||
} | ||
} | ||
|
||
// didToTerms converts a DID to a "tuple" of terms (either an Atom or a Variable), | ||
// or returns an error if the conversion fails. | ||
// The returned atoms are url decoded. | ||
func didToTerms(did *godid.DID) ([]engine.Term, error) { | ||
components := []string{did.Method, did.ID, did.Path, did.Query, did.Fragment} | ||
terms := make([]engine.Term, 0, len(components)) | ||
|
||
for _, component := range components { | ||
r, err := url.PathUnescape(component) | ||
if err != nil { | ||
return nil, err | ||
} | ||
terms = append(terms, util.StringToTerm(r)) | ||
} | ||
|
||
return terms, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
//nolint:gocognit | ||
package predicate | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/cosmos/cosmos-sdk/store" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
"github.com/ichiban/prolog/engine" | ||
"github.com/okp4/okp4d/x/logic/testutil" | ||
"github.com/okp4/okp4d/x/logic/types" | ||
. "github.com/smartystreets/goconvey/convey" | ||
"github.com/tendermint/tendermint/libs/log" | ||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types" | ||
tmdb "github.com/tendermint/tm-db" | ||
) | ||
|
||
func TestDID(t *testing.T) { | ||
Convey("Given a test cases", t, func() { | ||
cases := []struct { | ||
program string | ||
query string | ||
wantResult []types.TermResults | ||
wantError error | ||
}{ | ||
{ | ||
query: `did_components('did:example:123456',did(X,Y,_,_,_)).`, | ||
wantResult: []types.TermResults{{"X": "example", "Y": "'123456'"}}, | ||
}, | ||
{ | ||
query: `did_components('did:example:123456/path', X).`, | ||
wantResult: []types.TermResults{{"X": "did(example,'123456',path,_1,_2)"}}, | ||
}, | ||
{ | ||
query: `did_components('did:example:123456?versionId=1', X).`, | ||
wantResult: []types.TermResults{{"X": "did(example,'123456',_1,'versionId=1',_2)"}}, | ||
}, | ||
{ | ||
query: `did_components('did:example:123456/path%20with/space', X).`, | ||
wantResult: []types.TermResults{{"X": "did(example,'123456','path with/space',_1,_2)"}}, | ||
}, | ||
{ | ||
query: `did_components(X,did(example,'123456',_,'versionId=1',_)).`, | ||
wantResult: []types.TermResults{{"X": "'did:example:123456?versionId=1'"}}, | ||
}, | ||
{ | ||
query: `did_components(X,did(example,'123456','/foo/bar','versionId=1','test')).`, | ||
wantResult: []types.TermResults{{"X": "'did:example:123456/foo/bar?versionId=1#test'"}}, | ||
}, | ||
{ | ||
query: `did_components(X,did(example,'123456','path with/space',_,test)).`, | ||
wantResult: []types.TermResults{{"X": "'did:example:123456/path%20with/space#test'"}}, | ||
}, | ||
{ | ||
query: `did_components(X,Y).`, | ||
wantResult: []types.TermResults{}, | ||
wantError: fmt.Errorf("did_components/2: at least one argument must be instantiated"), | ||
}, | ||
{ | ||
query: `did_components('foo',X).`, | ||
wantResult: []types.TermResults{}, | ||
wantError: fmt.Errorf("did_components/2: invalid DID: input length is less than 7"), | ||
}, | ||
{ | ||
query: `did_components(123,X).`, | ||
wantResult: []types.TermResults{}, | ||
wantError: fmt.Errorf("did_components/2: cannot unify did with engine.Integer"), | ||
}, | ||
{ | ||
query: `did_components(X, 123).`, | ||
wantResult: []types.TermResults{}, | ||
wantError: fmt.Errorf("did_components/2: cannot unify did with engine.Integer"), | ||
}, | ||
{ | ||
query: `did_components(X,foo('bar')).`, | ||
wantResult: []types.TermResults{}, | ||
wantError: fmt.Errorf("did_components/2: invalid functor foo. Expected did"), | ||
}, | ||
{ | ||
query: `did_components(X,did('bar')).`, | ||
wantResult: []types.TermResults{}, | ||
wantError: fmt.Errorf("did_components/2: invalid arity 1. Expected 5"), | ||
}, | ||
{ | ||
query: `did_components('did:example:123456',foo(X)).`, | ||
wantResult: []types.TermResults{}, | ||
}, | ||
} | ||
for nc, tc := range cases { | ||
Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() { | ||
Convey("and a context", func() { | ||
db := tmdb.NewMemDB() | ||
stateStore := store.NewCommitMultiStore(db) | ||
ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) | ||
|
||
Convey("and a vm", func() { | ||
interpreter := testutil.NewInterpreterMust(ctx) | ||
interpreter.Register2(engine.NewAtom("did_components"), DIDComponents) | ||
|
||
err := interpreter.Compile(ctx, tc.program) | ||
So(err, ShouldBeNil) | ||
|
||
Convey("When the predicate is called", func() { | ||
sols, err := interpreter.QueryContext(ctx, tc.query) | ||
|
||
Convey("Then the error should be nil", func() { | ||
So(err, ShouldBeNil) | ||
So(sols, ShouldNotBeNil) | ||
|
||
Convey("and the bindings should be as expected", func() { | ||
var got []types.TermResults | ||
for sols.Next() { | ||
m := types.TermResults{} | ||
err := sols.Scan(m) | ||
So(err, ShouldBeNil) | ||
|
||
got = append(got, m) | ||
} | ||
if tc.wantError != nil { | ||
So(sols.Err(), ShouldNotBeNil) | ||
So(sols.Err().Error(), ShouldEqual, tc.wantError.Error()) | ||
} else { | ||
So(sols.Err(), ShouldBeNil) | ||
So(len(got), ShouldEqual, len(tc.wantResult)) | ||
for iGot, resultGot := range got { | ||
for varGot, termGot := range resultGot { | ||
So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.wantResult[iGot][varGot]) | ||
} | ||
} | ||
} | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
} | ||
}) | ||
} |
Oops, something went wrong.