Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: include detailed error on jsonld diff #30

Merged
merged 8 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions doc/ld/validator/testdata/extended_model.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"%s"
],
"id": "http://example.com/credentials/4643",
"type": [
"VerifiableCredential",
"CustomExt12"
],
"issuer": "https://example.com/issuers/14",
"issuanceDate": "2018-02-24T05:28:04Z",
"referenceNumber": 83294847,
"credentialSubject": [
{
"id": "did:example:abcdef1234567",
"name": "Jane Doe",
"favoriteFood": "Papaya"
}
]
}
12 changes: 12 additions & 0 deletions doc/ld/validator/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package validator

// Diff represents the difference between two objects.
type Diff struct {
OriginalValue interface{}
CompactedValue interface{}
}
82 changes: 68 additions & 14 deletions doc/ld/validator/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0
package validator

import (
json2 "encoding/json"
"errors"
"fmt"
"reflect"
Expand All @@ -18,10 +19,11 @@ import (
)

type validateOpts struct {
strict bool
jsonldDocumentLoader ld.DocumentLoader
externalContext []string
contextURIPositions []string
strict bool
jsonldDocumentLoader ld.DocumentLoader
externalContext []string
contextURIPositions []string
jsonldIncludeDetailedStructureDiffOnError bool
}

// ValidateOpts sets jsonld validation options.
Expand All @@ -34,6 +36,13 @@ func WithDocumentLoader(jsonldDocumentLoader ld.DocumentLoader) ValidateOpts {
}
}

// WithJSONLDIncludeDetailedStructureDiffOnError option is for including detailed structure diff in error message.
func WithJSONLDIncludeDetailedStructureDiffOnError() ValidateOpts {
return func(opts *validateOpts) {
opts.jsonldIncludeDetailedStructureDiffOnError = true
}
}

// WithExternalContext option is for definition of external context when doing JSON-LD operations.
func WithExternalContext(externalContext []string) ValidateOpts {
return func(opts *validateOpts) {
Expand Down Expand Up @@ -92,8 +101,17 @@ func ValidateJSONLDMap(docMap map[string]interface{}, options ...ValidateOpts) e
return fmt.Errorf("compact JSON-LD document: %w", err)
}

if opts.strict && !mapsHaveSameStructure(docMap, docCompactedMap) {
return errors.New("JSON-LD doc has different structure after compaction")
mapDiff := findMapDiff(docMap, docCompactedMap)
if opts.strict && len(mapDiff) != 0 {
errText := "JSON-LD doc has different structure after compaction"

if opts.jsonldIncludeDetailedStructureDiffOnError {
diff, _ := json2.Marshal(mapDiff) // nolint:errcheck

errText = fmt.Sprintf("%s. Details: %v", errText, string(diff))
}

return errors.New(errText)
}

err = validateContextURIPosition(opts.contextURIPositions, docMap)
Expand Down Expand Up @@ -136,40 +154,76 @@ func validateContextURIPosition(contextURIPositions []string, docMap map[string]
return nil
}

func mapsHaveSameStructure(originalMap, compactedMap map[string]interface{}) bool {
// nolint:gocyclo,funlen
func mapsHaveSameStructure(
originalMap,
compactedMap map[string]interface{},
path string,
) map[string][]*Diff {
original := compactMap(originalMap)
compacted := compactMap(compactedMap)

if reflect.DeepEqual(original, compacted) {
return true
return nil
}

diffs := make(map[string][]*Diff)

if len(original) != len(compacted) {
return false
for k, v := range original {
diffKey := path + "." + k
if _, ok := compacted[k]; !ok {
diffs[diffKey] = append(diffs[diffKey], &Diff{OriginalValue: v, CompactedValue: "!missing!"})
}
}

for k, v := range compacted {
diffKey := path + "." + k

if _, ok := original[k]; !ok {
diffs[diffKey] = append(diffs[diffKey], &Diff{OriginalValue: "!missing!", CompactedValue: v})
}
}

return diffs
}

for k, v1 := range original {
diffKey := path + "." + k

v1Map, isMap := v1.(map[string]interface{})
if !isMap {
continue
}

v2, present := compacted[k]
if !present { // special case - the name of the map was mapped, cannot guess what's a new name
continue
continue // should not be counted as diff
}

v2Map, isMap := v2.(map[string]interface{})
if !isMap {
return false
diffs[diffKey] = append(diffs[diffKey], &Diff{OriginalValue: v1, CompactedValue: v2})
}

if v2Map == nil {
v2Map = make(map[string]interface{})
}

if !mapsHaveSameStructure(v1Map, v2Map) {
return false
mp := mapsHaveSameStructure(v1Map, v2Map, diffKey)
for m1, m2 := range mp {
diffs[m1] = append(diffs[m1], m2...)
}
}

return true
return diffs
}

func findMapDiff(originalMap, compactedMap map[string]interface{}) map[string][]*Diff {
originalMap = compactMap(originalMap)
compactedMap = compactMap(compactedMap)

return mapsHaveSameStructure(originalMap, compactedMap, "$")
}

func compactMap(m map[string]interface{}) map[string]interface{} {
Expand Down
100 changes: 54 additions & 46 deletions doc/ld/validator/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ var (

//go:embed testdata/context/wallet_v1.jsonld
walletV1Context []byte

//go:embed testdata/extended_model.json
extendedModel string
)

func Test_ValidateJSONLD(t *testing.T) {
Expand Down Expand Up @@ -187,29 +190,7 @@ func Test_ValidateJSONLDWithExtraUndefinedSubjectFields(t *testing.T) {
t.Run("Extended basic VC model, credentialSubject is defined as object - undefined fields present",
func(t *testing.T) {
// Use a different VC to verify the case when credentialSubject is an array.
vcJSONTemplate := `
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"%s"
],
"id": "http://example.com/credentials/4643",
"type": [
"VerifiableCredential",
"CustomExt12"
],
"issuer": "https://example.com/issuers/14",
"issuanceDate": "2018-02-24T05:28:04Z",
"referenceNumber": 83294847,
"credentialSubject": [
{
"id": "did:example:abcdef1234567",
"name": "Jane Doe",
"favoriteFood": "Papaya"
}
]
}
`
vcJSONTemplate := extendedModel

vcJSON := fmt.Sprintf(vcJSONTemplate, contextURL)

Expand All @@ -218,31 +199,21 @@ func Test_ValidateJSONLDWithExtraUndefinedSubjectFields(t *testing.T) {
require.EqualError(t, err, "JSON-LD doc has different structure after compaction")
})

t.Run("Extended basic VC model, credentialSubject is defined as object - undefined fields present and details",
func(t *testing.T) {
// Use a different VC to verify the case when credentialSubject is an array.
vcJSONTemplate := extendedModel

vcJSON := fmt.Sprintf(vcJSONTemplate, contextURL)

err := ValidateJSONLD(vcJSON, WithDocumentLoader(loader), WithJSONLDIncludeDetailedStructureDiffOnError())
require.Error(t, err)
require.EqualError(t, err, "JSON-LD doc has different structure after compaction. Details: {\"$.credentialSubject\":[{\"OriginalValue\":{\"favoriteFood\":\"Papaya\",\"id\":\"did:example:abcdef1234567\",\"name\":\"Jane Doe\"},\"CompactedValue\":\"did:example:abcdef1234567\"}],\"$.credentialSubject.favoriteFood\":[{\"OriginalValue\":\"Papaya\",\"CompactedValue\":\"!missing!\"}],\"$.credentialSubject.id\":[{\"OriginalValue\":\"did:example:abcdef1234567\",\"CompactedValue\":\"!missing!\"}],\"$.credentialSubject.name\":[{\"OriginalValue\":\"Jane Doe\",\"CompactedValue\":\"!missing!\"}]}") //nolint:lll
})

t.Run("Extended basic VC model, credentialSubject is defined as array - undefined fields present", func(t *testing.T) {
// Use a different VC to verify the case when credentialSubject is an array.
vcJSONTemplate := `
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"%s"
],
"id": "http://example.com/credentials/4643",
"type": [
"VerifiableCredential",
"CustomExt12"
],
"issuer": "https://example.com/issuers/14",
"issuanceDate": "2018-02-24T05:28:04Z",
"referenceNumber": 83294847,
"credentialSubject": [
{
"id": "did:example:abcdef1234567",
"name": "Jane Doe",
"favoriteFood": "Papaya"
}
]
}
`
vcJSONTemplate := extendedModel

vcJSON := fmt.Sprintf(vcJSONTemplate, contextURL)

Expand Down Expand Up @@ -591,6 +562,43 @@ func Benchmark_ValidateJSONLD(b *testing.B) {
})
}

func TestDiffOnEduCred(t *testing.T) {
diff := findMapDiff(map[string]any{
"degree": map[string]any{
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts",
},
"alumniOf": map[string]any{
"id": "some-id",
"name": []any{
map[string]any{
"value": "University",
"lang": "en",
},
},
},
"type": []interface{}{
"VerifiableCredential",
"UniversityDegreeCredential",
},
}, map[string]any{
"degree": map[string]any{
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts",
},
"schema:alumniOf": map[string]any{
"id": "some-id",
"schema:name": []any{},
},
"type": []interface{}{
"VerifiableCredential",
"UniversityDegreeCredential",
},
})

require.Len(t, diff, 0)
}

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

Expand Down
Loading