Skip to content

Commit

Permalink
feat(logic): add did_components/2 predicate
Browse files Browse the repository at this point in the history
  • Loading branch information
ccamel committed Jan 31, 2023
1 parent 940858c commit 09976d9
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 0 deletions.
1 change: 1 addition & 0 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ var Registry = map[string]RegistryEntry{
"bank_balances/2": {predicate.BankBalances, 1},
"bank_spendable_balances/2": {predicate.BankSpendableBalances, 1},
"bank_locked_balances/2": {predicate.BankLockedBalances, 1},
"did_componets/2": {predicate.DIDComponents, 1},
}

// RegistryNames is the list of the predicate names in the Registry.
Expand Down
112 changes: 112 additions & 0 deletions x/logic/predicate/did.go
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:
c, err := godid.ParseDIDURL(t1.String())
if err != nil {
return engine.Error(fmt.Errorf("did_components/2: %w", err))
}

terms, err := didToTerms(c)
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
}
23 changes: 23 additions & 0 deletions x/logic/util/prolog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package util

import "github.com/ichiban/prolog/engine"

// StringToTerm converts a string to a term.
// If the string is empty, it returns a variable.
func StringToTerm(s string) engine.Term {
if s == "" {
return engine.NewVariable()
}

return engine.NewAtom(s)
}

// Resolve resolves a term and returns the resolved term and a boolean indicating whether the term is instantiated.
func Resolve(env *engine.Env, t engine.Term) (engine.Atom, bool) {
switch t := env.Resolve(t).(type) {
case engine.Atom:
return t, true
default:
return engine.NewAtom(""), false
}
}

0 comments on commit 09976d9

Please sign in to comment.