/home/runner/work/mcd/mcd/_site/ct/ct_run.ct_mcd@fv-az773-648.2023-11-24_16.44.07/mcd_protocol_text.COVER.html

1 %% Copyright (c) 2022 Peter Morgan <peter.james.morgan@gmail.com>
2 %%
3 %% Licensed under the Apache License, Version 2.0 (the "License");
4 %% you may not use this file except in compliance with the License.
5 %% You may obtain a copy of the License at
6 %%
7 %% http://www.apache.org/licenses/LICENSE-2.0
8 %%
9 %% Unless required by applicable law or agreed to in writing, software
10 %% distributed under the License is distributed on an "AS IS" BASIS,
11 %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 %% See the License for the specific language governing permissions and
13 %% limitations under the License.
14
15
16 -module(mcd_protocol_text).
17
18
19 -export([decode/2]).
20 -export([encode/1]).
21 -import(mcd_util, [split/1]).
22 -include("mcd.hrl").
23
24
25 decode(Command, Remainder)
26 when Command == set;
27 Command == cas;
28 Command == add;
29 Command == replace;
30 Command == append;
31 Command == prepend ->
32 59 decode_storage_command(Command, Remainder);
33
34 decode(Command, Remainder)
35 when Command == get;
36 Command == gets;
37 Command == gat;
38 Command == gats ->
39 47 decode_retrieval_command(Command, Remainder);
40
41 decode(delete = Command, Remainder) ->
42 7 [CommandLine, Encoded] = split(Remainder),
43 7 {mcd_re:run(
44 #{command => Command,
45 subject => CommandLine,
46 re => "(?<key>[^\\s]+)"
47 "( (?<noreply>noreply))?",
48 mapping => #{noreply => fun noreply/1}}),
49 Encoded};
50
51 decode(value = Command, Remainder) ->
52 42 [CommandLine, DataLine] = split(Remainder),
53 42 data_line(
54 mcd_re:run(
55 #{command => Command,
56 subject => CommandLine,
57 re => "(?<key>[^\\s]+) "
58 "(?<flags>\\d+) "
59 "(?<datalen>\\d+)"
60 "( (?<cas>\\d+))?",
61 mapping => #{flags => fun erlang:binary_to_integer/1,
62 cas => optional(fun binary_to_integer/1),
63 datalen => fun erlang:binary_to_integer/1}}),
64 DataLine);
65
66 decode(Command, Remainder)
67 when Command == client_error;
68 Command == server_error ->
69 4 [Reason, Encoded] = split(Remainder),
70 4 {#{command => Command, reason => Reason}, Encoded};
71
72 decode(touch = Command, Remainder) ->
73 1 [CommandLine, Encoded] = split(Remainder),
74 1 {mcd_re:run(
75 #{command => Command,
76 subject => CommandLine,
77 re => "(?<key>[^\\s]+) (?<expiry>[0-9]+)"
78 "( (?<noreply>noreply))?",
79 mapping => #{noreply => fun noreply/1,
80 expiry => fun erlang:binary_to_integer/1}}),
81 Encoded};
82
83 decode(stats = Command, Remainder) ->
84
:-(
[CommandLine, Encoded] = split(Remainder),
85
:-(
{mcd_re:run(
86 #{command => Command,
87 subject => CommandLine,
88 re => "(?<arg>\\w+)?",
89 mapping => #{arg => optional()}}),
90 Encoded};
91
92 decode(stat = Command, Remainder) ->
93
:-(
[CommandLine, Encoded] = split(Remainder),
94
:-(
{mcd_re:run(
95 #{command => Command,
96 subject => CommandLine,
97 re => "(?<key>[^\\s]+)\\s+(?<value>.+)"}),
98 Encoded};
99
100 decode(flush_all = Command, Remainder) ->
101 2 [CommandLine, Encoded] = split(Remainder),
102 2 {mcd_re:run(
103 #{command => Command,
104 subject => CommandLine,
105 re => "(\\s+(?<expiry>\\d+))?(\\s+(?<noreply>noreply))?",
106 mapping => #{expiry => optional_binary_to_integer(0),
107 noreply => fun noreply/1}}),
108 Encoded};
109
110 decode(quit = Command, Remainder) ->
111
:-(
[<<>>, Encoded] = split(Remainder),
112
:-(
{#{command => Command}, Encoded};
113
114 decode(verbosity = Command, Remainder) ->
115
:-(
[CommandLine, Encoded] = split(Remainder),
116
:-(
{mcd_re:run(
117 #{command => Command,
118 subject => CommandLine,
119 re => "(?<level>\\d+)( (?<noreply>noreply))?",
120 mapping => #{level => fun erlang:binary_to_integer/1,
121 noreply => fun noreply/1}}),
122 Encoded};
123
124 decode(incrdecr = Command, Remainder) ->
125 13 [CommandLine, Encoded] = split(Remainder),
126 13 {mcd_re:run(
127 #{command => Command,
128 subject => CommandLine,
129 re => "(?<value>\\d+)",
130 mapping => #{value => fun erlang:binary_to_integer/1}}),
131 Encoded};
132
133 decode(Command, Remainder)
134 when Command == incr;
135 Command == decr ->
136 23 [Parameters, Encoded] = split(Remainder),
137
138 23 {ok, MP} = re:compile(
139 "(?<key>[^\\s]+) (?<value>[0-9]+)"
140 "( (?<noreply>noreply))?"),
141 23 {namelist, NL} = re:inspect(MP, namelist),
142
143 23 case re:run(Parameters, MP, [{capture, all_names, binary}]) of
144 {match, Matches} ->
145 23 {lists:foldl(
146 fun
147 ({<<"noreply">>, Optional}, A) ->
148 23 A#{noreply => Optional /= <<>>};
149
150 ({<<"value">> = Name, Value}, A) ->
151 23 A#{binary_to_atom(Name) => binary_to_integer(Value)};
152
153 ({Name, Value}, A) ->
154 23 A#{binary_to_atom(Name) => Value}
155 end,
156 #{command => Command},
157 lists:zip(NL, Matches)),
158 Encoded}
159 end.
160
161
162 decode_storage_command(Command, Remainder)
163 when Command == set;
164 Command == add;
165 Command == replace;
166 Command == append;
167 Command == prepend ->
168 52 [CommandLine, DataBlock] = split(Remainder),
169 52 #{bytes := Size} =
170 Decoded =
171 re_run(
172 #{command => Command,
173 subject => CommandLine,
174 re => "(?<key>[^\\s]+) (?<flags>\\d+) (?<expiry>\\d+) (?<bytes>\\d+)( (?<noreply>noreply))?\\s*",
175 mapping => #{flags => fun erlang:binary_to_integer/1,
176 expiry => fun erlang:binary_to_integer/1,
177 noreply => fun noreply/1,
178 bytes => fun erlang:binary_to_integer/1}}),
179 52 case DataBlock of
180 <<Data:Size/bytes, ?RN, Encoded/bytes>> ->
181 52 {maps:without([bytes], Decoded#{data => Data}), Encoded};
182
183 _Otherwise ->
184
:-(
partial
185 end;
186
187 decode_storage_command(cas = Command, Remainder) ->
188 7 [CommandLine, DataLine] = split(Remainder),
189
190 7 data_line(
191 mcd_re:run(
192 #{command => Command,
193 subject => CommandLine,
194 re => "(?<key>[^\\s]+) (?<flags>\\d+) (?<expiry>\\d+) (?<datalen>\\d+) (?<unique>\\d+)( (?<noreply>noreply))?",
195 mapping => #{flags => fun erlang:binary_to_integer/1,
196 expiry => fun erlang:binary_to_integer/1,
197 unique => fun erlang:binary_to_integer/1,
198 noreply => fun noreply/1,
199 datalen => fun erlang:binary_to_integer/1}}),
200 DataLine).
201
202
203 noreply(Optional) ->
204 69 Optional /= <<>>.
205
206 optional() ->
207
:-(
?FUNCTION_NAME(fun identity/1).
208
209
:-(
identity(X) -> X.
210
211 optional(Mapping) when is_function(Mapping) ->
212 42 fun
213 (_, <<>>) ->
214 30 false;
215
216 (_, V) ->
217 12 {true, Mapping(V)}
218 end.
219
220
221 optional_binary_to_integer(Default) when is_integer(Default) ->
222 2 fun
223 (<<>>) ->
224 1 Default;
225
226 (Otherwise) ->
227 1 erlang:binary_to_integer(Otherwise)
228 end.
229
230
231 decode_retrieval_command(Command, Remainder)
232 when Command == get; Command == gets ->
233 44 case split(Remainder) of
234 [Keys, Encoded] ->
235 44 {#{command => Command,
236 keys => string:split(string:trim(Keys), <<" ">>, all)},
237 Encoded};
238
239 [_] ->
240
:-(
partial
241 end;
242
243 decode_retrieval_command(Command, Remainder)
244 when Command == gat; Command == gats ->
245 3 [CommandLine, Encoded] = split(Remainder),
246 3 {re_run(#{command => Command,
247 subject => CommandLine,
248 re => "(?<expiry>[0-9]+)\\s(?<keys>([^\\s]+\\s*)+)",
249 mapping => #{expiry => fun erlang:binary_to_integer/1,
250 keys => fun
251 (Keys) ->
252 3 string:split(Keys, " ", all)
253 end}}),
254 Encoded}.
255
256
257 re_run(#{subject := Subject, re := RE} = Arg) ->
258 55 {ok, MP} = re:compile(RE),
259 55 {namelist, NL} = re:inspect(MP, namelist),
260 55 case re:run(
261 Subject,
262 MP,
263 maps:get(
264 options,
265 Arg,
266 [{newline, crlf},
267 {capture, all_names, binary}])) of
268
269 {match, Matches} ->
270 55 lists:foldl(
271 fun
272 ({K, V}, A) ->
273 266 Key = binary_to_existing_atom(K),
274 266 A#{Key => case maps:find(
275 Key,
276 maps:get(mapping, Arg, #{})) of
277 {ok, Mapper} ->
278 214 Mapper(V);
279
280 error ->
281 52 V
282 end}
283 end,
284 maps:get(acc0, Arg, maps:with([command], Arg)),
285 lists:zip(NL, Matches));
286
287 nomatch ->
288
:-(
error(client_error)
289 end.
290
291
292 encode(#{command := ok = Command}) ->
293 2 [string:uppercase(atom_to_list(Command)), ?RN];
294
295 encode(#{command := Command}) when Command == stats;
296 Command == quit ->
297
:-(
[atom_to_list(Command), ?RN];
298
299 encode(#{command := stat = Command,
300 key := Key,
301 value := Value}) ->
302
:-(
[string:uppercase(atom_to_list(Command)), " ", Key, " ", Value, ?RN];
303
304 encode(#{command := incrdecr, value := Value}) ->
305 13 [integer_to_list(Value), ?RN];
306
307 encode(#{command := flush_all = Command,
308 expiry := Expiry,
309 noreply := Noreply}) ->
310 2 [atom_to_list(Command),
311 1 [[" ", integer_to_list(Expiry)] || Expiry > 0],
312
:-(
[" noreply" || Noreply],
313 ?RN];
314
315 encode(#{command := verbosity = Command,
316 level := Level,
317 noreply := Noreply}) ->
318
:-(
[atom_to_list(Command),
319 " ",
320 integer_to_list(Level),
321
:-(
[" noreply" || Noreply],
322 ?RN];
323
324 encode(#{command := Command,
325 keys := Keys}) when Command == get; Command == gets ->
326 44 [atom_to_list(Command),
327 " ",
328 lists:join(" ", Keys),
329 ?RN];
330
331 encode(#{command := Command,
332 key := Key,
333 flags := Flags,
334 expiry := Expiry,
335 noreply := Noreply,
336 data := Data}) when Command == set;
337 Command == add;
338 Command == replace;
339 Command == append;
340 Command == prepend ->
341 52 [io_lib:format("~p ~s ~p ~p ~p",
342 [Command, Key, Flags, Expiry, iolist_size(Data)]),
343 13 [" noreply" || Noreply],
344 ?RN,
345 Data,
346 ?RN];
347
348 encode(#{command := cas = Command,
349 key := Key,
350 flags := Flags,
351 expiry := Expiry,
352 unique := Unique,
353 noreply := Noreply,
354 data := Data}) ->
355 7 [io_lib:format("~p ~s ~p ~p ~p ~p",
356 [Command, Key, Flags, Expiry, iolist_size(Data), Unique]),
357 1 [" noreply" || Noreply],
358 ?RN,
359 Data,
360 ?RN];
361
362 encode(#{command := Command,
363 expiry := Expiry,
364 keys := Keys}) when Command == gat; Command == gats ->
365 3 [lists:join(
366 " ",
367 [atom_to_list(Command), integer_to_list(Expiry) | Keys]),
368 ?RN];
369
370 encode(#{command := delete, key := Key, noreply := Noreply}) ->
371 7 ["delete ", Key, [" noreply" || Noreply], ?RN];
372
373 encode(#{command := Command, key := Key, value := Value, noreply := Noreply})
374 when Command == incr; Command == decr ->
375 23 [io_lib:format("~p ~s ~p", [Command, Key, Value]),
376 6 [" noreply" || Noreply],
377 ?RN];
378
379 encode(#{command := touch = Command,
380 key := Key,
381 expiry := Expiry,
382 noreply := Noreply}) ->
383 1 [io_lib:format("~p ~s ~p", [Command, Key, Expiry]),
384
:-(
[" noreply" || Noreply],
385 ?RN];
386
387
388 encode(#{command := value, data := Data} = Arg) when is_integer(Data) ->
389 3 ?FUNCTION_NAME(Arg#{data := integer_to_binary(Data)});
390
391 encode(#{command := value,
392 key := Key,
393 cas := CAS,
394 flags := Flags,
395 data := Data}) ->
396 12 [io_lib:format(
397 "VALUE ~s ~p ~p ~p",
398 [Key,
399 Flags,
400 iolist_size(Data),
401 CAS]),
402 ?RN,
403 Data,
404 ?RN];
405
406 encode(#{command := value,
407 key := Key,
408 flags := Flags,
409 data := Data}) ->
410 30 [io_lib:format(
411 "VALUE ~s ~p ~p",
412 [Key,
413 Flags,
414 iolist_size(Data)]),
415 ?RN,
416 Data,
417 ?RN];
418
419 encode(#{command := 'end'}) ->
420 47 ["END", ?RN];
421
422 encode(#{command := error}) ->
423
:-(
["ERROR", ?RN];
424
425 encode(#{command := touched}) ->
426 1 ["TOUCHED", ?RN];
427
428 encode(#{command := client_error, reason := Reason}) ->
429 4 ["CLIENT_ERROR ", Reason, ?RN];
430
431 encode(#{command := server_error, reason := Reason}) ->
432
:-(
["SERVER_ERROR ", Reason, ?RN];
433
434 encode(#{command := stored}) ->
435 39 ["STORED", ?RN];
436
437 encode(#{command := not_stored}) ->
438 2 ["NOT_STORED", ?RN];
439
440 encode(#{command := exists}) ->
441 3 ["EXISTS", ?RN];
442
443 encode(#{command := not_found}) ->
444 4 ["NOT_FOUND", ?RN];
445
446 encode(#{command := deleted}) ->
447 4 ["DELETED", ?RN].
448
449
450 data_line(#{datalen := Length} = Decoded, DataLine) ->
451 49 case DataLine of
452 <<Data:Length/bytes, ?RN, Encoded/bytes>> ->
453 49 {maps:without([datalen], Decoded#{data => Data}), Encoded};
454
455 _ ->
456
:-(
partial
457 end.
Line Hits Source