Skip to content

Commit

Permalink
Merge pull request #31 from hyperledger-labs/feature/asset-datatype
Browse files Browse the repository at this point in the history
Implement ->@asset datatype
  • Loading branch information
samuelvenzi authored Oct 27, 2023
2 parents 6ea1747 + b60ed6b commit 4bcc3dc
Show file tree
Hide file tree
Showing 18 changed files with 370 additions and 30 deletions.
1 change: 1 addition & 0 deletions assets/assetProp.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type AssetProp struct {
// Primary types: "string", "number", "integer", "boolean", "datetime"
// Special types:
// -><assetType>: the specific asset type key (reference) as defined by <assetType> in the assets packages
// ->@asset: an arbitrary asset type key (reference)
// []<type>: an array of elements specified by <type> as any of the above valid types
DataType string `json:"dataType"`

Expand Down
3 changes: 3 additions & 0 deletions assets/assetType.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func (t AssetType) SubAssets() (subAssets []AssetProp) {
dataType := prop.DataType
dataType = strings.TrimPrefix(dataType, "[]")
dataType = strings.TrimPrefix(dataType, "->")
if dataType == "@asset" {
subAssets = append(subAssets, prop)
}
subAssetType := FetchAssetType(dataType)
if subAssetType != nil {
subAssets = append(subAssets, prop)
Expand Down
51 changes: 51 additions & 0 deletions assets/dataType.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"math"
"net/http"
"strconv"
"strings"
"time"

"github.com/hyperledger-labs/cc-tools/errors"
Expand All @@ -29,6 +30,11 @@ type DataType struct {

// CustomDataTypes allows cc developer to inject custom primitive data types
func CustomDataTypes(m map[string]DataType) error {
// Avoid initialization cycle
if FetchAssetType("->@asset") == nil {
dataTypeMap["->@asset"] = &assetDatatype
}

for k, v := range m {
if v.Parse == nil {
return errors.NewCCError(fmt.Sprintf("invalid custom data type '%s': nil Parse function", k), 500)
Expand Down Expand Up @@ -192,3 +198,48 @@ var dataTypeMap = map[string]*DataType{
},
},
}

var assetDatatype = DataType{
AcceptedFormats: []string{"->@asset"},
Parse: func(data interface{}) (string, interface{}, errors.ICCError) {
dataVal, ok := data.(map[string]interface{})
if !ok {
switch v := data.(type) {
case []byte:
err := json.Unmarshal(v, &dataVal)
if err != nil {
return "", nil, errors.WrapErrorWithStatus(err, "failed to unmarshal []byte into map[string]interface{}", http.StatusBadRequest)
}
case string:
err := json.Unmarshal([]byte(v), &dataVal)
if err != nil {
return "", nil, errors.WrapErrorWithStatus(err, "failed to unmarshal string into map[string]interface{}", http.StatusBadRequest)
}
default:
return "", nil, errors.NewCCError(fmt.Sprintf("asset property must be either a byte array or a string, but received type is: %T", data), http.StatusBadRequest)
}
}

key, er := GenerateKey(dataVal)
if er != nil {
return "", nil, errors.WrapError(er, "failed to generate key")
}
dataVal["@key"] = key

assetType, ok := dataVal["@assetType"].(string)
if ok {
if !strings.Contains(key, assetType) {
return "", nil, errors.NewCCError(fmt.Sprintf("asset type '%s' doesnt match key '%s'", assetType, key), http.StatusBadRequest)
}
} else {
dataVal["@assetType"] = key[:strings.IndexByte(key, ':')]
}

retVal, err := json.Marshal(dataVal)
if err != nil {
return "", nil, errors.WrapErrorWithStatus(err, "failed to marshal return value", http.StatusInternalServerError)
}

return string(retVal), dataVal, nil
},
}
3 changes: 3 additions & 0 deletions assets/dynamicAssetTypeFuncs.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ func CheckDataType(dataType string, newTypesList []interface{}) errors.ICCError

if strings.HasPrefix(trimDataType, "->") {
trimDataType = strings.TrimPrefix(trimDataType, "->")
if trimDataType == "@asset" {
return nil
}

assetType := FetchAssetType(trimDataType)
if assetType == nil {
Expand Down
13 changes: 8 additions & 5 deletions assets/generateKey.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,6 @@ func GenerateKey(asset map[string]interface{}) (string, errors.ICCError) {
keySeed += seed
} else {
// If key is a subAsset, generate subAsset's key to append to seed
assetTypeDef := FetchAssetType(dataTypeName)
if assetTypeDef == nil {
return "", errors.NewCCError(fmt.Sprintf("internal error: invalid sub asset type %s", prop.DataType), 500)
}

var propMap map[string]interface{}
switch t := propInterface.(type) {
case map[string]interface{}:
Expand All @@ -108,6 +103,14 @@ func GenerateKey(asset map[string]interface{}) (string, errors.ICCError) {
return "", errors.NewCCError(errMsg, 400)
}

if dataTypeName == "@asset" {
dataTypeName = propMap["@assetType"].(string)
}
assetTypeDef := FetchAssetType(dataTypeName)
if assetTypeDef == nil {
return "", errors.NewCCError(fmt.Sprintf("internal error: invalid sub asset type %s", prop.DataType), 500)
}

propMap["@assetType"] = dataTypeName
subAssetKey, err := GenerateKey(propMap)
if err != nil {
Expand Down
9 changes: 8 additions & 1 deletion assets/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,14 @@ func putRecursive(stub *sw.StubWrapper, object map[string]interface{}, root bool
// If subAsset is badly formatted, this method shouldn't have been called
return nil, errors.NewCCError(fmt.Sprintf("asset reference property '%s' must be an object", subAsset.Tag), 400)
}
obj["@assetType"] = dType
if dType != "@asset" {
obj["@assetType"] = dType
} else {
_, ok := obj["@assetType"].(string)
if !ok {
return nil, errors.NewCCError(fmt.Sprintf("asset reference property '%s' must have an '@assetType' property", subAsset.Tag), 400)
}
}
putSubAsset, err := putRecursive(stub, obj, false)
if err != nil {
return nil, errors.WrapError(err, fmt.Sprintf("failed to put sub-asset %s recursively", subAsset.Tag))
Expand Down
16 changes: 11 additions & 5 deletions assets/references.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,17 @@ func (a Asset) Refs() ([]Key, errors.ICCError) {
}

subAssetTypeName, ok := subAssetRefMap["@assetType"]
if ok && subAssetTypeName != subAssetDataType {
return nil, errors.NewCCError("sub-asset reference of wrong asset type", 400)
}
if !ok {
subAssetRefMap["@assetType"] = subAssetDataType
if subAssetDataType != "@asset" {
if ok && subAssetTypeName != subAssetDataType {
return nil, errors.NewCCError("sub-asset reference of wrong asset type", 400)
}
if !ok {
subAssetRefMap["@assetType"] = subAssetDataType
}
} else {
if !ok {
return nil, errors.NewCCError("sub-asset reference must have an '@assetType' property", 400)
}
}

// Generate key for subAsset
Expand Down
9 changes: 6 additions & 3 deletions assets/startupCheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ func StartupCheck() errors.ICCError {
// Check if there are references to undefined types
if isSubAsset {
// Checks if the prop's datatype exists on assetMap
propTypeDef := FetchAssetType(dataTypeName)
if propTypeDef == nil {
return errors.NewCCError(fmt.Sprintf("reference for undefined asset type '%s'", propDef.DataType), 500)
if dataTypeName != "@asset" {
propTypeDef := FetchAssetType(dataTypeName)
if propTypeDef == nil {
return errors.NewCCError(fmt.Sprintf("reference for undefined asset type '%s'", propDef.DataType), 500)
}
}

if propDef.DefaultValue != nil {
return errors.NewCCError(fmt.Sprintf("reference cannot have a default value in prop '%s' of asset '%s'", propDef.Label, assetType.Label), 500)
}
Expand Down
4 changes: 3 additions & 1 deletion assets/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,9 @@ func checkUpdateRecursive(stub *sw.StubWrapper, object map[string]interface{}, r
// If subAsset is badly formatted, this method shouldn't have been called
return errors.NewCCError(fmt.Sprintf("asset reference property '%s' must be an object", subAsset.Tag), 400)
}
obj["@assetType"] = dType
if dType != "@asset" {
obj["@assetType"] = dType
}
err := checkUpdateRecursive(stub, obj, false)
if err != nil {
return errors.WrapError(err, fmt.Sprintf("failed to check sub-asset %s recursively", subAsset.Tag))
Expand Down
35 changes: 27 additions & 8 deletions assets/validateProp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package assets

import (
"fmt"
"net/http"
"reflect"
"strings"

Expand Down Expand Up @@ -61,12 +62,6 @@ func validateProp(prop interface{}, propDef AssetProp) (interface{}, error) {
return nil, errors.WrapError(err, fmt.Sprintf("invalid '%s' (%s) asset property", propDef.Tag, propDef.Label))
}
} else {
// Check if type is defined in assetList
subAssetType := FetchAssetType(dataTypeName)
if subAssetType == nil {
return nil, errors.NewCCError(fmt.Sprintf("invalid asset type named '%s'", propDef.DataType), 400)
}

// Check if received subAsset is a map
var recvMap map[string]interface{}
switch t := prop.(type) {
Expand All @@ -80,8 +75,32 @@ func validateProp(prop interface{}, propDef AssetProp) (interface{}, error) {
return nil, errors.NewCCError("asset reference must be an object", 400)
}

// Add assetType to received object
recvMap["@assetType"] = dataTypeName
if dataTypeName != "@asset" {
// Check if type is defined in assetList
subAssetType := FetchAssetType(dataTypeName)
if subAssetType == nil {
return nil, errors.NewCCError(fmt.Sprintf("invalid asset type named '%s'", propDef.DataType), 400)
}

// Add assetType to received object
recvMap["@assetType"] = dataTypeName
} else {
keyStr, keyExists := recvMap["@key"].(string)
assetTypeStr, typeExists := recvMap["@assetType"].(string)
if !keyExists && !typeExists {
return nil, errors.NewCCError("invalid asset reference: missing '@key' or '@assetType' property", http.StatusBadRequest)
}
if keyExists {
assetTypeName := keyStr[:strings.IndexByte(keyStr, ':')]
if !typeExists {
recvMap["@assetType"] = assetTypeName
} else {
if assetTypeName != assetTypeStr {
return nil, errors.NewCCError("invalid asset reference: '@key' and '@assetType' properties do not match", http.StatusBadRequest)
}
}
}
}

// Check if all key props are included
key, err := NewKey(recvMap)
Expand Down
22 changes: 22 additions & 0 deletions test/assets_assetType_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ func TestAssetTypeToMap(t *testing.T) {
"dataType": "@object",
"writers": emptySlice,
},
{
"tag": "association",
"label": "Association",
"description": "",
"isKey": false,
"required": false,
"readOnly": false,
"defaultValue": nil,
"dataType": "[]->@asset",
"writers": emptySlice,
},
},
"readers": emptySlice,
"dynamic": false,
Expand Down Expand Up @@ -198,6 +209,17 @@ func TestAssetTypeListToMap(t *testing.T) {
"dataType": "@object",
"writers": emptySlice,
},
{
"tag": "association",
"label": "Association",
"description": "",
"isKey": false,
"required": false,
"readOnly": false,
"defaultValue": nil,
"dataType": "[]->@asset",
"writers": emptySlice,
},
},
"readers": emptySlice,
"dynamic": false,
Expand Down
87 changes: 86 additions & 1 deletion test/assets_dataType_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package test

import (
"encoding/json"
"log"
"net/http"
"reflect"
"testing"

Expand All @@ -21,7 +23,7 @@ func testParseValid(t *testing.T, dtype assets.DataType, inputVal interface{}, e
log.Printf("parsing %v expected key: %q but got %q\n", inputVal, expectedKey, key)
t.FailNow()
}
if val != expectedVal {
if !reflect.DeepEqual(val, expectedVal) {
log.Printf("parsing %v expected parsed val: \"%v\" of type %s but got \"%v\" of type %s\n", inputVal, expectedVal, reflect.TypeOf(expectedVal), val, reflect.TypeOf(val))
t.FailNow()
}
Expand Down Expand Up @@ -92,3 +94,86 @@ func TestDataTypeBoolean(t *testing.T) {
testParseInvalid(t, dtype, "True", 400)
testParseInvalid(t, dtype, 37.3, 400)
}

func TestDataTypeObject(t *testing.T) {
dtypeName := "@object"
dtype, exists := assets.DataTypeMap()[dtypeName]
if !exists {
log.Printf("%s datatype not declared in DataTypeMap\n", dtypeName)
t.FailNow()
}

testCase1 := map[string]interface{}{
"key1": "value1",
"key2": "value2",
}
testCaseByte1, _ := json.Marshal(testCase1)
testCaseExpected1 := map[string]interface{}{
"@assetType": "@object",
"key1": "value1",
"key2": "value2",
}
testCaseExpectedByte1, _ := json.Marshal(testCaseExpected1)

testParseValid(t, dtype, testCase1, string(testCaseExpectedByte1), testCase1)
testParseValid(t, dtype, testCaseByte1, string(testCaseExpectedByte1), testCase1)
testParseValid(t, dtype, string(testCaseByte1), string(testCaseExpectedByte1), testCase1)
testParseInvalid(t, dtype, "{'key': 'value'}", http.StatusBadRequest)
}
func TestDataTypeAsset(t *testing.T) {
dtypeName := "->@asset"
dtype, exists := assets.DataTypeMap()[dtypeName]
if !exists {
log.Printf("%s datatype not declared in DataTypeMap\n", dtypeName)
t.FailNow()
}

testCase1 := map[string]interface{}{
"@assetType": "person",
"id": "42186475006",
}
testCaseExpected1 := map[string]interface{}{
"@assetType": "person",
"id": "42186475006",
"@key": "person:a11e54a8-7e23-5d16-9fed-45523dd96bfa",
}
testCaseExpectedByte1, _ := json.Marshal(testCaseExpected1)
testParseValid(t, dtype, testCase1, string(testCaseExpectedByte1), testCaseExpected1)

testCase2 := map[string]interface{}{
"@assetType": "book",
"title": "Book Name",
"author": "Author Name",
"@key": "book:983a78df-9f0e-5ecb-baf2-4a8698590c81",
}
testCaseExpectedByte2, _ := json.Marshal(testCase2)
testParseValid(t, dtype, testCase2, string(testCaseExpectedByte2), testCase2)
testParseValid(t, dtype, testCaseExpectedByte2, string(testCaseExpectedByte2), testCase2)
testParseValid(t, dtype, string(testCaseExpectedByte2), string(testCaseExpectedByte2), testCase2)

testCase3 := map[string]interface{}{
"@key": "library:ca683ce5-05bf-5799-a359-b28a1f981f96",
}
testCaseExpected3 := map[string]interface{}{
"@assetType": "library",
"@key": "library:ca683ce5-05bf-5799-a359-b28a1f981f96",
}
testCaseExpectedByte3, _ := json.Marshal(testCaseExpected3)
testParseValid(t, dtype, testCase3, string(testCaseExpectedByte3), testCase3)

invalidCase1 := map[string]interface{}{
"@assetType": "library",
}
testParseInvalid(t, dtype, invalidCase1, http.StatusBadRequest)

invalidCase2 := map[string]interface{}{
"@assetType": "inexistant",
}
testParseInvalid(t, dtype, invalidCase2, http.StatusBadRequest)

invalidCase3 := map[string]interface{}{
"@assetType": "person",
"@key": "library:ca683ce5-05bf-5799-a359-b28a1f981f96",
}
testParseInvalid(t, dtype, invalidCase3, http.StatusBadRequest)
}
Loading

0 comments on commit 4bcc3dc

Please sign in to comment.