diff --git a/assets/assetProp.go b/assets/assetProp.go index a5b77f6..674e6c6 100644 --- a/assets/assetProp.go +++ b/assets/assetProp.go @@ -29,6 +29,7 @@ type AssetProp struct { // Primary types: "string", "number", "integer", "boolean", "datetime" // Special types: // ->: the specific asset type key (reference) as defined by in the assets packages + // ->@asset: an arbitrary asset type key (reference) // []: an array of elements specified by as any of the above valid types DataType string `json:"dataType"` diff --git a/assets/assetType.go b/assets/assetType.go index d98793c..6c0487f 100644 --- a/assets/assetType.go +++ b/assets/assetType.go @@ -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) diff --git a/assets/dataType.go b/assets/dataType.go index a429754..05ed2e9 100644 --- a/assets/dataType.go +++ b/assets/dataType.go @@ -6,6 +6,7 @@ import ( "math" "net/http" "strconv" + "strings" "time" "github.com/hyperledger-labs/cc-tools/errors" @@ -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) @@ -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 + }, +} diff --git a/assets/dynamicAssetTypeFuncs.go b/assets/dynamicAssetTypeFuncs.go index 2eb50c9..27102e8 100644 --- a/assets/dynamicAssetTypeFuncs.go +++ b/assets/dynamicAssetTypeFuncs.go @@ -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 { diff --git a/assets/generateKey.go b/assets/generateKey.go index 3f3708a..d96b10f 100644 --- a/assets/generateKey.go +++ b/assets/generateKey.go @@ -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{}: @@ -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 { diff --git a/assets/put.go b/assets/put.go index 8a2b40a..bb9ea99 100644 --- a/assets/put.go +++ b/assets/put.go @@ -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)) diff --git a/assets/references.go b/assets/references.go index d672ef6..7eb99eb 100644 --- a/assets/references.go +++ b/assets/references.go @@ -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 diff --git a/assets/startupCheck.go b/assets/startupCheck.go index d69651c..cb72da0 100644 --- a/assets/startupCheck.go +++ b/assets/startupCheck.go @@ -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) } diff --git a/assets/update.go b/assets/update.go index 2e46f4f..945d943 100644 --- a/assets/update.go +++ b/assets/update.go @@ -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)) diff --git a/assets/validateProp.go b/assets/validateProp.go index 11ce1cf..5b347d3 100644 --- a/assets/validateProp.go +++ b/assets/validateProp.go @@ -2,6 +2,7 @@ package assets import ( "fmt" + "net/http" "reflect" "strings" @@ -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) { @@ -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) diff --git a/test/assets_assetType_test.go b/test/assets_assetType_test.go index 2270398..760557a 100644 --- a/test/assets_assetType_test.go +++ b/test/assets_assetType_test.go @@ -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, @@ -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, diff --git a/test/assets_dataType_test.go b/test/assets_dataType_test.go index 2f1d1e4..7556559 100644 --- a/test/assets_dataType_test.go +++ b/test/assets_dataType_test.go @@ -1,7 +1,9 @@ package test import ( + "encoding/json" "log" + "net/http" "reflect" "testing" @@ -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() } @@ -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) +} diff --git a/test/chaincode_test.go b/test/chaincode_test.go index de57b85..ee0fea9 100644 --- a/test/chaincode_test.go +++ b/test/chaincode_test.go @@ -76,6 +76,12 @@ var testAssetList = []assets.AssetType{ Label: "Other Info", DataType: "@object", }, + { + // Generic JSON object + Tag: "association", + Label: "Association", + DataType: "[]->@asset", + }, }, }, { diff --git a/test/tx_createAsset_test.go b/test/tx_createAsset_test.go index 0540a76..d9ccb7b 100644 --- a/test/tx_createAsset_test.go +++ b/test/tx_createAsset_test.go @@ -88,6 +88,112 @@ func TestCreateAsset(t *testing.T) { } } +func TestCreateAssetGenericAssociation(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + book := map[string]interface{}{ + "@assetType": "book", + "title": "Book Title", + "author": "Author", + } + + library := map[string]interface{}{ + "@assetType": "library", + "name": "Library Name", + } + + req := map[string]interface{}{ + "asset": []map[string]interface{}{book, library}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAsset", [][]byte{ + []byte("createAsset"), + reqBytes, + }) + + person := map[string]interface{}{ + "@assetType": "person", + "name": "Maria", + "id": "318.207.920-48", + "association": []map[string]interface{}{book, library}, + } + req = map[string]interface{}{ + "asset": []map[string]interface{}{person}, + } + reqBytes, err = json.Marshal(req) + if err != nil { + t.FailNow() + } + + res = stub.MockInvoke("createAsset", [][]byte{ + []byte("createAsset"), + reqBytes, + }) + lastUpdated, _ := stub.GetTxTimestamp() + expectedResponse := map[string]interface{}{ + "@key": "person:47061146-c642-51a1-844a-bf0b17cb5e19", + "@lastTouchBy": "org1MSP", + "@lastTx": "createAsset", + "@lastUpdated": lastUpdated.AsTime().Format(time.RFC3339), + "@assetType": "person", + "name": "Maria", + "id": "31820792048", + "height": 0.0, + "association": []interface{}{ + map[string]interface{}{ + "@assetType": "book", + "@key": "book:46179ee0-5503-54e1-aa51-bbaad559638b", + }, + map[string]interface{}{ + "@assetType": "library", + "@key": "library:9aeaddc4-d1cb-5b03-9ad0-1d9af7416c2e", + }, + }, + } + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + var resPayload []map[string]interface{} + err = json.Unmarshal(res.GetPayload(), &resPayload) + if err != nil { + log.Println(err) + t.FailNow() + } + + if len(resPayload) != 1 { + log.Println("response length should be 1") + t.FailNow() + } + + if !reflect.DeepEqual(resPayload[0], expectedResponse) { + log.Println("these should be equal") + log.Printf("%#v\n", resPayload[0]) + log.Printf("%#v\n", expectedResponse) + t.FailNow() + } + + var state map[string]interface{} + stateBytes := stub.State["person:47061146-c642-51a1-844a-bf0b17cb5e19"] + err = json.Unmarshal(stateBytes, &state) + if err != nil { + log.Println(err) + t.FailNow() + } + + if !reflect.DeepEqual(state, expectedResponse) { + log.Println("these should be equal") + log.Printf("%#v\n", state) + log.Printf("%#v\n", expectedResponse) + t.FailNow() + } +} + func TestCreateAssetEmptyList(t *testing.T) { stub := mock.NewMockStub("org1MSP", new(testCC)) diff --git a/test/tx_getDataTypes_test.go b/test/tx_getDataTypes_test.go index 48b06c4..41d1639 100644 --- a/test/tx_getDataTypes_test.go +++ b/test/tx_getDataTypes_test.go @@ -51,6 +51,12 @@ func TestGetDataTypes(t *testing.T) { }, "DropDownValues": nil, }, + "->@asset": map[string]interface{}{ + "acceptedFormats": []interface{}{ + "->@asset", + }, + "DropDownValues": nil, + }, } err := invokeAndVerify(stub, "getDataTypes", nil, expectedResponse, 200) if err != nil { diff --git a/test/tx_getSchema_test.go b/test/tx_getSchema_test.go index 1b1f2a1..baf8b4a 100644 --- a/test/tx_getSchema_test.go +++ b/test/tx_getSchema_test.go @@ -120,6 +120,16 @@ func TestGetSchema(t *testing.T) { "tag": "info", "writers": nil, }, + map[string]interface{}{ + "dataType": "[]->@asset", + "description": "", + "isKey": false, + "label": "Association", + "readOnly": false, + "required": false, + "tag": "association", + "writers": nil, + }, }, } err = invokeAndVerify(stub, "getSchema", req, expectedPersonSchema, 200) diff --git a/transactions/getArgs.go b/transactions/getArgs.go index 9e3cc69..d100121 100644 --- a/transactions/getArgs.go +++ b/transactions/getArgs.go @@ -3,6 +3,7 @@ package transactions import ( "encoding/json" "fmt" + "net/http" "strings" "github.com/hyperledger-labs/cc-tools/assets" @@ -122,11 +123,17 @@ func validateTxArg(argType string, arg interface{}) (interface{}, errors.ICCErro return nil, errors.NewCCError("invalid argument format", 400) } assetTypeName, ok := argMap["@assetType"] - if ok && assetTypeName != argType { // in case an @assetType is specified, check if it is correct - return nil, errors.NewCCError(fmt.Sprintf("invalid @assetType '%s' (expecting '%s')", assetTypeName, argType), 400) - } - if !ok { // if @assetType is not specified, inject it - argMap["@assetType"] = argType + if argType != "@asset" { + if ok && assetTypeName != argType { // in case an @assetType is specified, check if it is correct + return nil, errors.NewCCError(fmt.Sprintf("invalid @assetType '%s' (expecting '%s')", assetTypeName, argType), 400) + } + if !ok { // if @assetType is not specified, inject it + argMap["@assetType"] = argType + } + } else { + if !ok { + return nil, errors.NewCCError("missing @assetType", http.StatusBadRequest) + } } key, err := assets.NewKey(argMap) if err != nil { diff --git a/transactions/startupCheck.go b/transactions/startupCheck.go index fe8a731..1fea9ac 100644 --- a/transactions/startupCheck.go +++ b/transactions/startupCheck.go @@ -41,7 +41,7 @@ func StartupCheck() errors.ICCError { dtype != "@object" { if strings.HasPrefix(dtype, "->") { dtype = strings.TrimPrefix(dtype, "->") - if assets.FetchAssetType(dtype) == nil { + if assets.FetchAssetType(dtype) == nil && dtype != "@asset" { return errors.NewCCError(fmt.Sprintf("invalid arg type %s in tx %s", arg.DataType, txName), 500) } } else {