Skip to content

Commit

Permalink
feat: add logic for jsonld types validation (#41)
Browse files Browse the repository at this point in the history
* feat: add logic for jsonld types validation

* fix: lint

* fix: lint

* feat: more tests

* fix: lint
  • Loading branch information
skynet2 authored Dec 10, 2024
1 parent 6ae560f commit b8ce698
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 7 deletions.
30 changes: 27 additions & 3 deletions doc/ld/processor/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,32 @@ func AppendExternalContexts(context interface{}, extraContexts ...string) []inte
}

// Compact compacts given json ld object.
func (p *Processor) Compact(input, context map[string]interface{},
opts ...Opts) (map[string]interface{}, error) {
func (p *Processor) Compact(
input,
context map[string]interface{},
opts ...Opts,
) (map[string]interface{}, error) {
options, context := p.getContextAndOptions(input, context, opts...)

return ld.NewJsonLdProcessor().Compact(input, context, options)
}

// Expand expands given json ld object.
func (p *Processor) Expand(
input,
context map[string]interface{},
opts ...Opts,
) ([]interface{}, error) {
options, _ := p.getContextAndOptions(input, context, opts...)

return ld.NewJsonLdProcessor().Expand(input, options)
}

func (p *Processor) getContextAndOptions(
input map[string]any,
context map[string]any,
opts ...Opts,
) (*ld.JsonLdOptions, map[string]any) {
procOptions := prepareOpts(opts)

ldOptions := ld.NewJsonLdOptions("")
Expand All @@ -193,7 +217,7 @@ func (p *Processor) Compact(input, context map[string]interface{},
context = map[string]interface{}{"@context": inputContext}
}

return ld.NewJsonLdProcessor().Compact(input, context, ldOptions)
return ldOptions, context
}

// Frame makes a frame from the inputDoc using frameDoc.
Expand Down
23 changes: 23 additions & 0 deletions doc/ld/validator/testdata/credential_with_invalid_type.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/security/suites/ed25519-2020/v1"
],
"id": "urn:uuid:86294362-4254-4f36-854f-3952fe42555d",
"type": [
"VerifiableCredential",
"UnknownType"
],
"issuer": "did:key:z6MkptjaoxjyKQFSqf1dHXswP6EayYhPQBYzprVCPmGBHz9S",
"issuanceDate": "2020-03-16T22:37:26.544Z",
"credentialSubject": {
"id": "did:key:z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b"
},
"proof": {
"type": "Ed25519Signature2020",
"created": "2024-12-09T15:40:18Z",
"verificationMethod": "did:key:z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b#z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b",
"proofPurpose": "assertionMethod",
"proofValue": "z2vToPLpbp8D4rrv8xaqhv4kXsfnAJoVhBTzfyJFnfnayiPmPWnWL7arPiFUMSuWnJquK3t2hCP4Rqqjs2QGEctgP"
}
}
82 changes: 82 additions & 0 deletions doc/ld/validator/testdata/credential_with_valid_type.json

Large diffs are not rendered by default.

94 changes: 90 additions & 4 deletions doc/ld/validator/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,20 @@ func ValidateJSONLD(doc string, options ...ValidateOpts) error {
}

// ValidateJSONLDMap validates jsonld structure.
func ValidateJSONLDMap(docMap map[string]interface{}, options ...ValidateOpts) error {
func ValidateJSONLDMap(
docMap map[string]interface{},
options ...ValidateOpts,
) error {
opts := getValidateOpts(options)

jsonldProc := processor.Default()

docCompactedMap, err := jsonldProc.Compact(docMap,
nil, processor.WithDocumentLoader(opts.jsonldDocumentLoader),
processor.WithExternalContext(opts.externalContext...))
docCompactedMap, err := jsonldProc.Compact(
docMap,
nil,
processor.WithDocumentLoader(opts.jsonldDocumentLoader),
processor.WithExternalContext(opts.externalContext...),
)
if err != nil {
return fmt.Errorf("compact JSON-LD document: %w", err)
}
Expand All @@ -122,6 +128,86 @@ func ValidateJSONLDMap(docMap map[string]interface{}, options ...ValidateOpts) e
return nil
}

// ValidateJSONLDTypes validates JSON-LD types.
func ValidateJSONLDTypes(
docMap map[string]interface{},
options ...ValidateOpts,
) error {
typesObj, typeObjOk := docMap["type"]

if !typeObjOk {
return nil
}

types, typesOk := typesObj.([]interface{})
if !typesOk {
return errors.New("type must be an array")
}

if len(types) == 0 {
return nil
}

documentTypes := map[string]struct{}{}
for _, t := range types {
documentTypes[fmt.Sprint(t)] = struct{}{}
}

jsonldProc := processor.Default()
opts := getValidateOpts(options)

docExpanded, err := jsonldProc.Expand(
docMap,
nil,
processor.WithDocumentLoader(opts.jsonldDocumentLoader),
processor.WithExternalContext(opts.externalContext...),
)
if err != nil {
return errors.Join(err, errors.New("expand JSON-LD document"))
}

return validateTypesInExpandedDoc(docExpanded, documentTypes)
}

func validateTypesInExpandedDoc(
docExpanded []any,
types map[string]struct{},
) error {
if len(docExpanded) != 1 {
return fmt.Errorf("expanded document must contain only one element, got %d", len(docExpanded))
}

docExpandedMap, ok := docExpanded[0].(map[string]interface{})
if !ok {
return fmt.Errorf("document must be a map, got %s", reflect.TypeOf(docExpanded[0]).String())
}

expandedTypesObj, expandedTypesObjOk := docExpandedMap["@type"]
if !expandedTypesObjOk {
return errors.New("expanded document does not contain @type")
}

expandedTypes, expandedTypesOk := expandedTypesObj.([]interface{})
if !expandedTypesOk {
return errors.New("expanded @type must be an array")
}

if len(expandedTypes) == 0 {
return nil
}

for _, t := range expandedTypes {
if _, typeOk := types[fmt.Sprint(t)]; typeOk {
// expand should change types to full URIs.
// example "VerifiableCredential" -> "https://www.w3.org/2018/credentials#VerifiableCredential".
return fmt.Errorf("expanded document contains unexpanded type %s. "+
"All types should be declared in contexts", t)
}
}

return nil
}

func validateContextURIPosition(contextURIPositions []string, docMap map[string]interface{}) error {
if len(contextURIPositions) == 0 {
return nil
Expand Down
77 changes: 77 additions & 0 deletions doc/ld/validator/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ package validator

import (
_ "embed"
"encoding/json"
"fmt"
"net/http"
"testing"

"github.com/piprate/json-gold/ld"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

ldcontext "github.com/trustbloc/did-go/doc/ld/context"
Expand Down Expand Up @@ -599,6 +603,79 @@ func TestDiffOnEduCred(t *testing.T) {
require.Len(t, diff, 0)
}

//go:embed testdata/credential_with_invalid_type.json
var credentialWithInvalidType []byte

//go:embed testdata/credential_with_valid_type.json
var credentialWithValidType []byte

func TestValidateType(t *testing.T) {
loader := ld.NewCachingDocumentLoader(ld.NewDefaultDocumentLoader(http.DefaultClient))

t.Run("invalid type", func(t *testing.T) {
var parsed map[string]interface{}
assert.NoError(t, json.Unmarshal(credentialWithInvalidType, &parsed))

err := ValidateJSONLDTypes(parsed, WithDocumentLoader(loader))
assert.ErrorContains(
t,
err,
"expanded document contains unexpanded type UnknownType. All types should be declared in contexts",
)
})

t.Run("no types", func(t *testing.T) {
var parsed map[string]interface{}
assert.NoError(t, json.Unmarshal([]byte(`{"a" : "b"}`), &parsed))

assert.NoError(t, ValidateJSONLDTypes(parsed, WithDocumentLoader(loader)))
})

t.Run("valid", func(t *testing.T) {
var parsed map[string]interface{}
assert.NoError(t, json.Unmarshal(credentialWithValidType, &parsed))

assert.NoError(t, ValidateJSONLDTypes(parsed, WithDocumentLoader(loader)))
})

t.Run("invalid length", func(t *testing.T) {
assert.ErrorContains(t, validateTypesInExpandedDoc([]any{
1,
2,
}, nil), "expanded document must contain only one element")
})

t.Run("invalid type", func(t *testing.T) {
assert.ErrorContains(t, validateTypesInExpandedDoc([]any{
1,
}, nil), "document must be a map")
})

t.Run("no type", func(t *testing.T) {
assert.ErrorContains(t, validateTypesInExpandedDoc([]any{
map[string]any{
"xx": "yy",
},
}, nil), "expanded document does not contain")
})

t.Run("no type", func(t *testing.T) {
assert.ErrorContains(t, validateTypesInExpandedDoc([]any{
map[string]any{
"@type": "yy",
},
}, nil), "expanded @type must be an array")
})

t.Run("no records", func(t *testing.T) {
assert.NoError(t, validateTypesInExpandedDoc([]any{
map[string]any{
"@type": []any{},
},
}, nil))
})
}

func createTestDocumentLoader(t *testing.T, extraContexts ...ldcontext.Document) *ldloader.DocumentLoader {
t.Helper()

Expand Down

0 comments on commit b8ce698

Please sign in to comment.