diff --git a/doc/ld/validator/testdata/extended_model.json b/doc/ld/validator/testdata/extended_model.json new file mode 100644 index 0000000..2ff44d7 --- /dev/null +++ b/doc/ld/validator/testdata/extended_model.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/doc/ld/validator/types.go b/doc/ld/validator/types.go new file mode 100644 index 0000000..7e51467 --- /dev/null +++ b/doc/ld/validator/types.go @@ -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{} +} diff --git a/doc/ld/validator/validate.go b/doc/ld/validator/validate.go index b99a72b..a1b2fe8 100644 --- a/doc/ld/validator/validate.go +++ b/doc/ld/validator/validate.go @@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0 package validator import ( + json2 "encoding/json" "errors" "fmt" "reflect" @@ -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. @@ -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) { @@ -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) @@ -136,19 +154,43 @@ 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 @@ -156,20 +198,32 @@ func mapsHaveSameStructure(originalMap, compactedMap map[string]interface{}) boo 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{} { diff --git a/doc/ld/validator/validate_test.go b/doc/ld/validator/validate_test.go index 47338e3..825b176 100644 --- a/doc/ld/validator/validate_test.go +++ b/doc/ld/validator/validate_test.go @@ -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) { @@ -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) @@ -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) @@ -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()