diff --git a/v2/arangodb/collection_documents_delete_impl.go b/v2/arangodb/collection_documents_delete_impl.go index 25275088..9a12837e 100644 --- a/v2/arangodb/collection_documents_delete_impl.go +++ b/v2/arangodb/collection_documents_delete_impl.go @@ -120,20 +120,37 @@ func (c *collectionDocumentDeleteResponseReader) Read(i interface{}) (Collection var meta CollectionDocumentDeleteResponse - if c.options != nil { - meta.Old = c.options.OldObject - } + var response Unmarshal[shared.ResponseStruct, Unmarshal[DocumentMeta, UnmarshalData]] - if err := c.array.Unmarshal(newMultiUnmarshaller(&meta, newUnmarshalInto(i))); err != nil { + if err := c.array.Unmarshal(&response); err != nil { if err == io.EOF { return CollectionDocumentDeleteResponse{}, shared.NoMoreDocumentsError{} } return CollectionDocumentDeleteResponse{}, err } + if q := response.Current; q != nil { + meta.ResponseStruct = *q + } + + if q := response.Object.Current; q != nil { + meta.DocumentMeta = *q + } + if meta.Error != nil && *meta.Error { return meta, meta.AsArangoError() } + if err := response.Object.Object.Inject(i); err != nil { + return CollectionDocumentDeleteResponse{}, err + } + + if c.options != nil && c.options.OldObject != nil { + meta.Old = c.options.OldObject + if err := response.Object.Object.Extract("old").Inject(meta.Old); err != nil { + return CollectionDocumentDeleteResponse{}, err + } + } + return meta, nil } diff --git a/v2/arangodb/collection_documents_read_impl.go b/v2/arangodb/collection_documents_read_impl.go index fa5f2188..6a30bfb8 100644 --- a/v2/arangodb/collection_documents_read_impl.go +++ b/v2/arangodb/collection_documents_read_impl.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2020-2024 ArangoDB GmbH, Cologne, Germany +// Copyright 2020-2025 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -74,23 +74,24 @@ func (c collectionDocumentRead) ReadDocument(ctx context.Context, key string, re func (c collectionDocumentRead) ReadDocumentWithOptions(ctx context.Context, key string, result interface{}, opts *CollectionDocumentReadOptions) (DocumentMeta, error) { url := c.collection.url("document", key) - var response struct { - shared.ResponseStruct `json:",inline"` - DocumentMeta `json:",inline"` - } - - data := newUnmarshalInto(result) + var response Unmarshal[shared.ResponseStruct, Unmarshal[DocumentMeta, UnmarshalData]] - resp, err := connection.CallGet(ctx, c.collection.connection(), url, newMultiUnmarshaller(&response, data), c.collection.withModifiers(opts.modifyRequest)...) + resp, err := connection.CallGet(ctx, c.collection.connection(), url, &response, c.collection.withModifiers(opts.modifyRequest)...) if err != nil { return DocumentMeta{}, err } switch code := resp.Code(); code { case http.StatusOK: - return response.DocumentMeta, nil + if err := response.Object.Object.Inject(result); err != nil { + return DocumentMeta{}, err + } + if z := response.Object.Current; z != nil { + return *z, nil + } + return DocumentMeta{}, nil default: - return DocumentMeta{}, response.AsArangoErrorWithCode(code) + return DocumentMeta{}, response.Current.AsArangoErrorWithCode(code) } } @@ -112,18 +113,32 @@ func (c *collectionDocumentReadResponseReader) Read(i interface{}) (CollectionDo return CollectionDocumentReadResponse{}, shared.NoMoreDocumentsError{} } - var meta CollectionDocumentReadResponse + var response Unmarshal[shared.ResponseStruct, Unmarshal[DocumentMeta, UnmarshalData]] - if err := c.array.Unmarshal(newMultiUnmarshaller(&meta, newUnmarshalInto(i))); err != nil { + if err := c.array.Unmarshal(&response); err != nil { if err == io.EOF { return CollectionDocumentReadResponse{}, shared.NoMoreDocumentsError{} } return CollectionDocumentReadResponse{}, err } + var meta CollectionDocumentReadResponse + + if q := response.Current; q != nil { + meta.ResponseStruct = *q + } + + if q := response.Object.Current; q != nil { + meta.DocumentMeta = *q + } + if meta.Error != nil && *meta.Error { return meta, meta.AsArangoError() } + if err := response.Object.Object.Inject(i); err != nil { + return CollectionDocumentReadResponse{}, err + } + return meta, nil } diff --git a/v2/arangodb/collection_indexes_impl.go b/v2/arangodb/collection_indexes_impl.go index 922bff55..7ec72804 100644 --- a/v2/arangodb/collection_indexes_impl.go +++ b/v2/arangodb/collection_indexes_impl.go @@ -228,12 +228,9 @@ func (c *collectionIndexes) EnsureInvertedIndex(ctx context.Context, options *In func (c *collectionIndexes) ensureIndex(ctx context.Context, reqData interface{}, result interface{}) (bool, error) { urlEndpoint := c.collection.db.url("_api", "index") - var response struct { - shared.ResponseStruct `json:",inline"` - } - data := newUnmarshalInto(&result) + var response Unmarshal[shared.ResponseStruct, UnmarshalData] - resp, err := connection.CallPost(ctx, c.collection.connection(), urlEndpoint, newMultiUnmarshaller(&response, data), &reqData, + resp, err := connection.CallPost(ctx, c.collection.connection(), urlEndpoint, &response, &reqData, c.collection.withModifiers(connection.WithQuery("collection", c.collection.name))...) if err != nil { return false, errors.WithStack(err) @@ -241,11 +238,17 @@ func (c *collectionIndexes) ensureIndex(ctx context.Context, reqData interface{} switch code := resp.Code(); code { case http.StatusOK: + if err := response.Object.Inject(result); err != nil { + return false, err + } return false, nil case http.StatusCreated: + if err := response.Object.Inject(result); err != nil { + return false, err + } return true, nil default: - return false, response.AsArangoErrorWithCode(code) + return false, response.Current.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/cursor_impl.go b/v2/arangodb/cursor_impl.go index 656d8d40..c57cf7dd 100644 --- a/v2/arangodb/cursor_impl.go +++ b/v2/arangodb/cursor_impl.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2020-2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2020-2025 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -144,22 +144,21 @@ func (c *cursor) readDocument(ctx context.Context, result interface{}) (Document } } - var data byteDecoder - if err := c.data.Result.Read(&data); err != nil { + var res Unmarshal[DocumentMeta, UnmarshalData] + + if err := c.data.Result.Read(&res); err != nil { return DocumentMeta{}, err } - var meta DocumentMeta - - if err := data.Unmarshal(&meta); err != nil { - // Ignore error + if err := res.Object.Inject(result); err != nil { + return DocumentMeta{}, err } - if err := data.Unmarshal(result); err != nil { - return DocumentMeta{}, err + if m := res.Current; m != nil { + return *m, nil } - return meta, nil + return DocumentMeta{}, nil } func (c *cursor) getNextBatch(ctx context.Context, retryBatchID string) error { diff --git a/v2/arangodb/database_transaction_impl.go b/v2/arangodb/database_transaction_impl.go index c32fe788..7aa9fc5c 100644 --- a/v2/arangodb/database_transaction_impl.go +++ b/v2/arangodb/database_transaction_impl.go @@ -60,13 +60,17 @@ func (d databaseTransaction) listTransactionsWithStatuses(ctx context.Context, s } `json:"transactions,omitempty"` } - var response shared.ResponseStruct + var response Unmarshal[shared.ResponseStruct, UnmarshalData] - resp, err := connection.CallGet(ctx, d.db.connection(), url, newMultiUnmarshaller(&result, &response), d.db.modifiers...) + resp, err := connection.CallGet(ctx, d.db.connection(), url, &response, d.db.modifiers...) if err != nil { return nil, errors.WithStack(err) } + if err := response.Object.Inject(&result); err != nil { + return nil, err + } + switch code := resp.Code(); code { case http.StatusOK: var t []Transaction @@ -80,7 +84,7 @@ func (d databaseTransaction) listTransactionsWithStatuses(ctx context.Context, s return t, nil default: - return nil, response.AsArangoErrorWithCode(code) + return nil, response.Current.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/unmarshaller.go b/v2/arangodb/unmarshaller.go new file mode 100644 index 00000000..30118a38 --- /dev/null +++ b/v2/arangodb/unmarshaller.go @@ -0,0 +1,109 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package arangodb + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +type Unmarshaler interface { + json.Unmarshaler + + Inject(object any) error + Extract(key string) Unmarshaler +} + +type Unmarshal[C, T any] struct { + Current *C + + Object T +} + +func (u *Unmarshal[C, T]) UnmarshalJSON(bytes []byte) error { + u.Current = nil + + var q C + + if err := json.Unmarshal(bytes, &q); err == nil { + u.Current = &q + } + + return json.Unmarshal(bytes, &u.Object) +} + +type UnmarshalData []byte + +func (u *UnmarshalData) Inject(object any) error { + if u == nil { + return errors.Errorf("Data provided is nil") + } + + return json.Unmarshal(*u, object) +} + +func (u *UnmarshalData) Extract(key string) Unmarshaler { + if u == nil { + return errorUnmarshalData{err: errors.Errorf("Data provided is nil")} + } + + var z map[string]UnmarshalData + + if err := json.Unmarshal(*u, &z); err != nil { + return errorUnmarshalData{err: err} + } + + if v, ok := z[key]; ok { + return &v + } + + return errorUnmarshalData{err: errors.Errorf("Key %s not found", key)} +} + +func (u *UnmarshalData) UnmarshalJSON(bytes []byte) error { + if u == nil { + return errors.Errorf("Data provided is nil") + } + + z := make([]byte, len(bytes)) + + copy(z, bytes) + + *u = z + return nil +} + +type errorUnmarshalData struct { + err error +} + +func (e errorUnmarshalData) UnmarshalJSON(bytes []byte) error { + return e.err +} + +func (e errorUnmarshalData) Inject(object any) error { + return e.err +} + +func (e errorUnmarshalData) Extract(key string) Unmarshaler { + return e +} diff --git a/v2/arangodb/utils.go b/v2/arangodb/utils.go index c062a7d0..a3a72a5d 100644 --- a/v2/arangodb/utils.go +++ b/v2/arangodb/utils.go @@ -30,65 +30,6 @@ import ( "github.com/pkg/errors" ) -var _ json.Unmarshaler = &multiUnmarshaller{} -var _ json.Marshaler = &multiUnmarshaller{} - -func newMultiUnmarshaller(obj ...interface{}) json.Unmarshaler { - return &multiUnmarshaller{ - obj: obj, - } -} - -type multiUnmarshaller struct { - obj []interface{} -} - -func (m multiUnmarshaller) MarshalJSON() ([]byte, error) { - r := map[string]interface{}{} - for _, o := range m.obj { - z := map[string]interface{}{} - if d, err := json.Marshal(o); err != nil { - return nil, err - } else { - if err := json.Unmarshal(d, &z); err != nil { - return nil, err - } - } - - for k, v := range z { - r[k] = v - } - } - - return json.Marshal(r) -} - -func (m multiUnmarshaller) UnmarshalJSON(d []byte) error { - for _, o := range m.obj { - if err := json.Unmarshal(d, o); err != nil { - return err - } - } - - return nil -} - -type byteDecoder struct { - data []byte -} - -func (b *byteDecoder) UnmarshalJSON(d []byte) error { - b.data = make([]byte, len(d)) - - copy(b.data, d) - - return nil -} - -func (b *byteDecoder) Unmarshal(i interface{}) error { - return json.Unmarshal(b.data, i) -} - func newUnmarshalInto(obj interface{}) *UnmarshalInto { return &UnmarshalInto{obj} } diff --git a/v2/tests/database_collection_doc_create_code_test.go b/v2/tests/database_collection_doc_create_code_test.go new file mode 100644 index 00000000..27095945 --- /dev/null +++ b/v2/tests/database_collection_doc_create_code_test.go @@ -0,0 +1,105 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package tests + +import ( + "context" + "testing" + + "github.com/arangodb/go-driver/v2/arangodb/shared" + + "github.com/stretchr/testify/require" + + "github.com/arangodb/go-driver/v2/arangodb" +) + +type DocWithCode struct { + Key string `json:"_key,omitempty"` + Code string `json:"code"` +} + +func Test_DatabaseCollectionDocCreateCode(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContextT(t, defaultTestTimeout, func(ctx context.Context, tb testing.TB) { + doc := DocWithCode{ + Key: "test", + } + + meta, err := col.CreateDocumentWithOptions(ctx, doc, &arangodb.CollectionDocumentCreateOptions{}) + require.NoError(t, err) + require.NotEmpty(t, meta.Rev) + require.Empty(t, meta.Old) + require.Empty(t, meta.New) + + rdoc, err := col.ReadDocument(ctx, "test", &doc) + require.NoError(t, err) + + require.EqualValues(t, "test", rdoc.Key) + }) + }) + }) + + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContextT(t, defaultTestTimeout, func(ctx context.Context, tb testing.TB) { + doc := DocWithCode{ + Key: "test", + } + doc2 := DocWithCode{ + Key: "test2", + } + + _, err := col.CreateDocuments(ctx, []any{ + doc, doc2, + }) + require.NoError(t, err) + + docs, err := col.ReadDocuments(ctx, []string{ + "test", + "tz44", + "test2", + }) + require.NoError(t, err) + + var z DocWithCode + + meta, err := docs.Read(&z) + require.NoError(t, err) + require.EqualValues(t, "test", meta.Key) + + _, err = docs.Read(&z) + require.Error(t, err) + require.True(t, shared.IsNotFound(err)) + + meta, err = docs.Read(&z) + require.NoError(t, err) + require.EqualValues(t, "test2", meta.Key) + + _, err = docs.Read(&z) + require.Error(t, err) + require.True(t, shared.IsNoMoreDocuments(err)) + }) + }) + }) + }) +} diff --git a/v2/tests/utils_client_test.go b/v2/tests/utils_client_test.go index 04bb8473..48cc0e84 100644 --- a/v2/tests/utils_client_test.go +++ b/v2/tests/utils_client_test.go @@ -59,7 +59,12 @@ func createAuthenticationFromEnv(t testing.TB, conn connection.Connection) conne // getEndpointsFromEnv returns the endpoints specified in the TEST_ENDPOINTS environment variable. func getEndpointsFromEnv(t testing.TB) []string { - eps := strings.Split(os.Getenv("TEST_ENDPOINTS"), ",") + v, ok := os.LookupEnv("TEST_ENDPOINTS") + if !ok { + t.Fatal("No endpoints found in environment variable TEST_ENDPOINTS") + } + + eps := strings.Split(v, ",") if len(eps) == 0 { t.Fatal("No endpoints found in environment variable TEST_ENDPOINTS") }