Skip to content

Commit

Permalink
feat: include detailed error on jsonld diff (#30)
Browse files Browse the repository at this point in the history
* feat: compact details

* fix: go mod

* feat: manual compare

* feat: include detailed error

* fix: lint

* feat: ensure map not nil

* fix: lint

* fix: find diff
  • Loading branch information
skynet2 authored Aug 12, 2024
1 parent 59149bb commit c7d31e6
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 60 deletions.
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

0 comments on commit c7d31e6

Please sign in to comment.