Skip to content

Commit

Permalink
refactor(logic)!: did_components/2 now returns encoded components
Browse files Browse the repository at this point in the history
  • Loading branch information
ccamel committed Feb 14, 2024
1 parent 5b5a33e commit e6cd0fc
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 93 deletions.
117 changes: 39 additions & 78 deletions x/logic/predicate/did.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package predicate

import (
"net/url"
"strings"

"github.com/ichiban/prolog/engine"
godid "github.com/nuts-foundation/go-did/did"
"github.com/samber/lo"

"github.com/okp4/okp4d/x/logic/prolog"
)

// AtomDID is a term which represents a DID as a compound term `did(Method, ID, Path, Query, Fragment)`.
var AtomDID = engine.NewAtom("did")

// DIDPrefix is the prefix for a DID.
const DIDPrefix = "did:"
const DIDPrefix = "did"

// DIDComponents is a predicate which breaks down a DID into its components according to the [W3C DID] specification.
//
Expand All @@ -24,19 +21,19 @@ const DIDPrefix = "did:"
// did_components(-DID, +Components) is det
//
// where:
// - DID represents DID URI, given as an Atom, compliant with [W3C DID] specification.
// - DID represent DID URI, given as an Atom, compliant with [W3C DID] specification.
// - Components is a compound Term in the format did(Method, ID, Path, Query, Fragment), aligned with the [DID syntax],
// where: Method is The method name, ID is The method-specific identifier, Path is the path component, Query is the
// query component and Fragment is The fragment component.
// query component and Fragment is The fragment component. Values are given as an Atom and are url encoded.
// For any component not present, its value will be null and thus will be left as an uninstantiated variable.
//
// Examples:
//
// # Decompose a DID into its components.
// - did_components('did:example:123456?versionId=1', did(Method, ID, Path, Query, Fragment)).
// - did_components('did:example:123456?versionId=1', did_components(Method, ID, Path, Query, Fragment)).
//
// # Reconstruct a DID from its components.
// - did_components(DID, did('example', '123456', _, 'versionId=1', _42)).
// - did_components(DID, did_components('example', '123456', _, 'versionId=1', _42)).
//
// [W3C DID]: https://w3c.github.io/did-core
// [DID syntax]: https://w3c.github.io/did-core/#did-syntax
Expand All @@ -51,12 +48,15 @@ func DIDComponents(vm *engine.VM, did, components engine.Term, cont engine.Cont,
return engine.Error(prolog.WithError(engine.DomainError(prolog.ValidEncoding("did"), did, env), err, env))
}

terms, err := didToTerms(parsedDid, env)
if err != nil {
return engine.Error(err)
}
terms := lo.Map([]string{parsedDid.Method, parsedDid.ID, parsedDid.Path, parsedDid.Query.Encode(), parsedDid.Fragment},
func(segment string, _ int) engine.Term {
if segment == "" {
return engine.NewVariable()
}
return engine.NewAtom(segment)
})

return engine.Unify(vm, components, AtomDID.Apply(terms...), cont, env)
return engine.Unify(vm, components, prolog.AtomDIDComponents.Apply(terms...), cont, env)
default:
return engine.Error(engine.TypeError(prolog.AtomTypeAtom, did, env))
}
Expand All @@ -65,80 +65,41 @@ func DIDComponents(vm *engine.VM, did, components engine.Term, cont engine.Cont,
case engine.Variable:
return engine.Error(engine.InstantiationError(env))
case engine.Compound:
if t2.Functor() != AtomDID || t2.Arity() != 5 {
return engine.Error(engine.DomainError(AtomDID, components, env))
if t2.Functor() != prolog.AtomDIDComponents || t2.Arity() != 5 {
return engine.Error(engine.DomainError(prolog.AtomDIDComponents, components, env))
}

buf := strings.Builder{}
buf.WriteString(DIDPrefix)

processors := []func(engine.Atom){
func(segment engine.Atom) {
buf.WriteString(segment.String())
},
func(segment engine.Atom) {
buf.WriteString(":")
buf.WriteString(url.PathEscape(segment.String()))
},
func(segment engine.Atom) {
for _, s := range strings.FieldsFunc(segment.String(), func(c rune) bool { return c == '/' }) {
buf.WriteString("/")
buf.WriteString(url.PathEscape(s))
}
},
func(segment engine.Atom) {
buf.WriteString("?")
buf.WriteString(url.PathEscape(segment.String()))
},
func(segment engine.Atom) {
buf.WriteString("#")
buf.WriteString(url.PathEscape(segment.String()))
},
}

for i := 0; i < t2.Arity(); i++ {
if err := processSegment(t2, uint8(i), processors[i], env); err != nil {
return engine.Error(err)
sep := ""
switch i {
case 0, 1:
sep = ":"
case 2:
sep = "/"
case 3:
sep = "?"
case 4:
sep = "#"
}
switch segment := env.Resolve(t2.Arg(i)).(type) {
case engine.Variable:
default:
atom, err := prolog.AssertAtom(env, segment)
if err != nil {
return engine.Error(err)
}
if !strings.HasPrefix(atom.String(), sep) {
buf.WriteString(sep)
}
buf.WriteString(atom.String())
}
}

return engine.Unify(vm, did, engine.NewAtom(buf.String()), cont, env)
default:
return engine.Error(engine.TypeError(AtomDID, components, env))
return engine.Error(engine.TypeError(prolog.AtomDIDComponents, components, env))
}
}

// processSegment processes a segment of a DID.
func processSegment(segments engine.Compound, segmentNumber uint8, fn func(segment engine.Atom), env *engine.Env) error {
term := env.Resolve(segments.Arg(int(segmentNumber)))
if _, ok := term.(engine.Variable); ok {
return nil
}
segment, err := prolog.AssertAtom(env, segments.Arg(int(segmentNumber)))
if err != nil {
return err
}

fn(segment)

return nil
}

// 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, env *engine.Env) ([]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, engine.DomainError(prolog.ValidEncoding("url_encoded"), engine.NewAtom(component), env)
}
var r2 engine.Term = engine.NewAtom(r)
terms = append(terms, r2)
}

return terms, nil
}
34 changes: 19 additions & 15 deletions x/logic/predicate/did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,37 +32,41 @@ func TestDID(t *testing.T) {
wantError error
}{
{
query: `did_components('did:example:123456',did(X,Y,_,_,_)).`,
query: `did_components('did:example:123456',did_components(X,Y,_,_,_)).`,
wantResult: []types.TermResults{{"X": "example", "Y": "'123456'"}},
},
{
query: `did_components('did:example:123456',did(X,Y,Z,_,_)).`,
wantResult: []types.TermResults{{"X": "example", "Y": "'123456'", "Z": "''"}},
query: `did_components('did:example:123456',did_components(X,Y,Z,_,_)).`,
wantResult: []types.TermResults{{"X": "example", "Y": "'123456'", "Z": "_1"}},
},
{
query: `did_components('did:example:123456/path', X).`,
wantResult: []types.TermResults{{"X": "did(example,'123456',path,'','')"}},
wantResult: []types.TermResults{{"X": "did_components(example,'123456',path,_1,_2)"}},
},
{
query: `did_components('did:example:123456?versionId=1', X).`,
wantResult: []types.TermResults{{"X": "did(example,'123456','','versionId=1','')"}},
wantResult: []types.TermResults{{"X": "did_components(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','','')"}},
wantResult: []types.TermResults{{"X": "did_components(example,'123456','path%20with/space',_1,_2)"}},
},
{
query: `did_components(X,did(example,'123456',_,'versionId=1',_)).`,
query: `did_components(X,did_components(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')).`,
query: `did_components(X,did_components(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)).`,
query: `did_components(X,did_components(example,'123456','path%20with/space',_,test)).`,
wantResult: []types.TermResults{{"X": "'did:example:123456/path%20with/space#test'"}},
},
{
query: `did_components(X,did_components(example,'123456','/foo/bar','version%20Id=1','test')).`,
wantResult: []types.TermResults{{"X": "'did:example:123456/foo/bar?version%20Id=1#test'"}},
},
{
query: `did_components(X,Y).`,
wantResult: []types.TermResults{},
Expand All @@ -72,7 +76,7 @@ func TestDID(t *testing.T) {
query: `did_components('foo',X).`,
wantResult: []types.TermResults{},
wantError: fmt.Errorf("error(domain_error(encoding(did),foo),[%s],did_components/2)",
strings.Join(strings.Split("invalid DID: input length is less than 7", ""), ",")),
strings.Join(strings.Split("invalid DID", ""), ",")),
},
{
query: `did_components(123,X).`,
Expand All @@ -82,20 +86,20 @@ func TestDID(t *testing.T) {
{
query: `did_components(X, 123).`,
wantResult: []types.TermResults{},
wantError: fmt.Errorf("error(type_error(did,123),did_components/2)"),
wantError: fmt.Errorf("error(type_error(did_components,123),did_components/2)"),
},
{
query: `did_components(X,foo('bar')).`,
wantResult: []types.TermResults{},
wantError: fmt.Errorf("error(domain_error(did,foo(bar)),did_components/2)"),
wantError: fmt.Errorf("error(domain_error(did_components,foo(bar)),did_components/2)"),
},
{
query: `did_components(X,did('bar')).`,
query: `did_components(X,did_components('bar')).`,
wantResult: []types.TermResults{},
wantError: fmt.Errorf("error(domain_error(did,did(bar)),did_components/2)"),
wantError: fmt.Errorf("error(domain_error(did_components,did_components(bar)),did_components/2)"),
},
{
query: `did_components(X,did(example,'123456','path with/space',5,test)).`,
query: `did_components(X,did_components(example,'123456','path with/space',5,test)).`,
wantResult: []types.TermResults{},
wantError: fmt.Errorf("error(type_error(atom,5),did_components/2)"),
},
Expand Down
2 changes: 2 additions & 0 deletions x/logic/prolog/atom.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ var (
AtomAs = engine.NewAtom("as")
// AtomAt are terms with principal functor (@)/1 used to represent special values in json objects.
AtomAt = engine.NewAtom("@")
// AtomDIDComponents is a term which represents a DID as a compound term `did_components(Method, ID, Path, Query, Fragment)`.
AtomDIDComponents = engine.NewAtom("did_components")
// AtomDot is the term used to represent the dot in a list.
AtomDot = engine.NewAtom(".")
// AtomEmpty is the term used to represent empty.
Expand Down

0 comments on commit e6cd0fc

Please sign in to comment.