Skip to content

Commit fcf86e5

Browse files
committed
readme: update README.md
The commit completely rewrites decoding section since the decoder was reworked. Along the way, commit marks all snippets as `c++` code and drops `conn.status.is_failed` from docs since it doesn't exist anymore. Closes #94
1 parent 3cfffe0 commit fcf86e5

File tree

2 files changed

+48
-90
lines changed

2 files changed

+48
-90
lines changed

README.md

Lines changed: 48 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Connector can be embedded in any C++ application with including main header:
112112
To create client one should specify buffer's and network provider's implementations
113113
as template parameters. Connector's main class has the following signature:
114114

115-
```
115+
```c++
116116
template<class BUFFER, class NetProvider = EpollNetProvider<BUFFER>>
117117
class Connector;
118118
```
@@ -122,7 +122,7 @@ one can use default one: `tnt::Buffer<16 * 1024>` and
122122
`EpollNetProvider<tnt::Buffer<16 * 1024>>`.
123123
So the default instantiation would look
124124
like:
125-
```
125+
```c++
126126
using Buf_t = tnt::Buffer<16 * 1024>;
127127
using Net_t = EpollNetProvider<Buf_t >;
128128
Connector<Buf_t, Net_t> client;
@@ -131,7 +131,7 @@ Connector<Buf_t, Net_t> client;
131131
Client itself is not enough to work with Tarantool instances, so let's also create
132132
connection objects. Connection takes buffer and network provider as template
133133
parameters as well (note that they must be the same as ones of client):
134-
```
134+
```c++
135135
Connection<Buf_t, Net_t> conn(client);
136136
```
137137
@@ -140,17 +140,18 @@ Connection<Buf_t, Net_t> conn(client);
140140
Now assume Tarantool instance is listening `3301` port on localhost. To connect
141141
to the server we should invoke `Connector::connect()` method of client object and
142142
pass three arguments: connection instance, address and port.
143-
`int rc = client.connect(conn, address, port)`.
143+
```c++
144+
int rc = client.connect(conn, address, port);
145+
```
144146

145147
### Error handling
146148

147149
Implementation of connector is exception
148150
free, so we rely on return codes: in case of fail, `connect()` will return `rc < 0`.
149151
To get error message corresponding to the last error happened during communication
150152
with server, we can invoke `Connection::getError()` method:
151-
```
153+
```c++
152154
if (rc != 0) {
153-
assert(conn.status.is_failed);
154155
std::cerr << conn.getError() << std::endl;
155156
}
156157
```
@@ -161,16 +162,18 @@ one can use `Connection::reset()`.
161162
### Preparing requests
162163

163164
To execute simplest request (i.e. ping), one can invoke corresponding method of
164-
connection object:
165-
`rid_t ping = conn.ping();`
165+
connection object:
166+
```c++
167+
rid_t ping = conn.ping();
168+
```
166169
Each request method returns request id, which is sort of future. It can be used
167170
to get the result of request execution once it is ready (i.e. response). Requests
168171
are queued in the input buffer of connection until `Connector::wait()` is called.
169172

170173
### Sending requests
171174

172175
That said, to send requests to the server side, we should invoke `client.wait()`:
173-
```
176+
```c++
174177
client.wait(conn, ping, WAIT_TIMEOUT);
175178
```
176179
Basically, `wait()` takes connection to poll (both IN and OUT), request id and
@@ -187,7 +190,7 @@ in case response is not ready yet). Note that on each future it can be called
187190
only once: `getResponse()` erases request id from internal map once it is
188191
returned to user.
189192

190-
```
193+
```c++
191194
std::optional<Response<Buf_t>> response = conn.getResponse(ping);
192195
```
193196
Response consists of header and body (`response.header` and `response.body`).
@@ -204,103 +207,59 @@ Assume we have space with `id = 512` and following format on the server:
204207
`CREATE TABLE t(id INT PRIMARY KEY, a TEXT, b DOUBLE);`
205208
Preparing analogue of `t:replace(1, "111", 1.01);` request can be done this way:
206209

207-
```
210+
```c++
208211
std::tuple data = std::make_tuple(1 /* field 1*/, "111" /* field 2*/, 1.01 /* field 3*/);
209212
rid_t replace = conn.space[512].replace(data);
210213
```
211214
To execute select query `t.index[1]:select({1}, {limit = 1})`:
212215

213-
```
216+
```c++
214217
auto i = conn.space[512].index[1];
215218
rid_t select = i.select(std::make_tuple(1), 1, 0 /*offset*/, IteratorType::EQ);
216219
```
217220

218221
### Data readers
219222

220-
Responses from server contain raw data (i.e. encoded into msgpuck tuples). To
221-
decode client's data, users have to write their own decoders (based on featured
222-
schema). Let's define structure describing data stored in space `t`:
223+
Responses from server contain raw data (i.e. encoded into MsgPack tuples).
224+
Let's define structure describing data stored in space `t`:
223225

224-
```
226+
```c++
225227
struct UserTuple {
226228
uint64_t field1;
227229
std::string field2;
228230
double field3;
229-
};
230-
```
231231

232-
Prototype of the base reader is given in `src/mpp/Dec.hpp`:
233-
```
234-
template <class BUFFER, Type TYPE>
235-
struct SimpleReaderBase : DefaultErrorHandler {
236-
using BufferIterator_t = typename BUFFER::iterator;
237-
/* Allowed type of values to be parsed. */
238-
static constexpr Type VALID_TYPES = TYPE;
239-
BufferIterator_t* StoreEndIterator() { return nullptr; }
240-
};
241-
```
242-
So every new reader should inherit from it or directly from `DefaultErrorHandler`.
243-
To parse particular value, we should define `Value()` method. First two arguments
244-
are common and unused as a rule, but the third - defines parsed value. So in
245-
case of POD stuctures it's enough to provide byte-to-byte copy. Since in our
246-
schema there are fields of three different types, let's descripe three `Value()`
247-
functions:
248-
```
249-
struct UserTupleValueReader : mpp::DefaultErrorHandler {
250-
/* Store instance of tuple to be parsed. */
251-
UserTuple& tuple;
252-
/* Enumerate all types which can be parsed. Otherwise */
253-
static constexpr mpp::Type VALID_TYPES = mpp::MP_UINT | mpp::MP_STR | mpp::MP_DBL;
254-
UserTupleValueReader(UserTuple& t) : tuple(t) {}
255-
256-
/* Value's extractors. */
257-
void Value(const BufIter_t&, mpp::compact::Type, uint64_t u)
258-
{
259-
tuple.field1 = u;
260-
}
261-
void Value(const BufIter_t&, mpp::compact::Type, double d)
262-
{
263-
tuple.field3 = d;
264-
}
265-
void Value(const BufIter_t& itr, mpp::compact::Type, mpp::StrValue v)
266-
{
267-
BufIter_t tmp = itr;
268-
tmp += v.offset;
269-
std::string &dst = tuple.field2;
270-
while (v.size) {
271-
dst.push_back(*tmp);
272-
++tmp;
273-
--v.size;
274-
}
275-
}
232+
static constexpr auto mpp = std::make_tuple(
233+
&UserTuple::field1, &UserTuple::field2, &UserTuple::field3);
276234
};
277235
```
278-
It is worth mentioning that tuple itself is wrapped into array, so in fact
279-
firstly we should parse array. Let's define another one reader:
280-
```
281-
template <class BUFFER>
282-
struct UserTupleReader : mpp::SimpleReaderBase<BUFFER, mpp::MP_ARR> {
283-
mpp::Dec<BUFFER>& dec;
284-
UserTuple& tuple;
285-
286-
UserTupleReader(mpp::Dec<BUFFER>& d, UserTuple& t) : dec(d), tuple(t) {}
287-
void Value(const iterator_t<BUFFER>&, mpp::compact::Type, mpp::ArrValue)
288-
{
289-
dec.SetReader(false, UserTupleValueReader{tuple});
290-
}
291-
};
292-
```
293-
`SetReader();` sets the reader which is invoked while every entry of the array is
294-
parsed. Now, to make these two readers work, we should create decoder, set
295-
its iterator to the position of encoded tuple and invoke `Read()` method:
296-
```
297-
UserTuple tuple;
298-
mpp::Dec dec(conn.getInBuf());
299-
dec.SetPosition(*t.begin);
300-
dec.SetReader(false, UserTupleReader<BUFFER>{dec, tuple});
301-
dec.Read();
302-
```
303236
304-
### Writing custom buffer and network provider
237+
Member `mpp` is neccessary - it sets the relationship between the structure
238+
members and associated tuple's fields. It is used by encoder and decoder
239+
for Object <-> MsgPack serialization. For instance, such structure will be
240+
serialied as a MsgPack array `[<field1>, <field2>, <field3>]`. If you need
241+
to serialize non-static members of objects,
242+
[pointers to data members](https://en.cppreference.com/w/cpp/language/pointer#Pointers_to_data_members)
243+
can be used, just as in this example.
244+
245+
Let's get back to the example with `select`. Consider the request successful.
246+
We can decode data in this way:
247+
248+
```c++
249+
if (response.body.data != std::nullopt) {
250+
std::vector<UserTuple> results;
251+
bool ok = response.body.data->decode(results);
252+
if (ok)
253+
print_results(results);
254+
}
255+
```
305256

306-
TODO
257+
Firstly, we check if the response actually contains any data (Tarantool has
258+
sent `IPROTO_DATA` in response). According to
259+
[`IPROTO` protocol](https://www.tarantool.io/ru/doc/latest/reference/internals/box_protocol/),
260+
key `IPROTO_DATA`
261+
[has](https://www.tarantool.io/ru/doc/latest/reference/internals/iproto/format/#body)
262+
an array of tuples as value in response to `select`. So, in order to
263+
successfully decode them, we should pass an array of tuples to decoder - that's
264+
why `std::vector<UserTuple>` is needed. If decoding was successful, `results`
265+
will contain all decoded `UserTuples`.

doc/tntcxx_api.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,6 @@ Public methods
420420
int rc = client.connect(conn, address, port);
421421
422422
if (rc != 0) {
423-
assert(conn.status.is_failed);
424423
std::cerr << conn.getError() << std::endl;
425424
return -1;
426425
}

0 commit comments

Comments
 (0)