Line data Source code
1 : /***************************************************************************\
2 : * Name : C++ header dumper *
3 : * Description : generate C++ header with all structs and enums *
4 : * Author : antonin.kriz@gmail.com *
5 : * ------------------------------------------------------------------------- *
6 : * This is free software; you can redistribute it and/or modify it under the *
7 : * terms of the MIT license. A copy of the license can be found in the file *
8 : * "LICENSE" at the root of this distribution. *
9 : \***************************************************************************/
10 :
11 : #include "header.h"
12 : #include "../parser/char_stream.h"
13 : #include "ast/ast.h"
14 : #include "ast/proto-common.h"
15 : #include "ast/proto-field.h"
16 : #include "ast/proto-file.h"
17 : #include "ast/proto-import.h"
18 : #include "ast/proto-message.h"
19 : #include "io/file.h"
20 : #include "parser/options.h"
21 : #include "parser/parser.h"
22 : #include <algorithm>
23 : #include <array>
24 : #include <cctype>
25 : #include <functional>
26 : #include <initializer_list>
27 : #include <spb/json/deserialize.hpp>
28 : #include <sstream>
29 : #include <stdexcept>
30 : #include <string>
31 : #include <string_view>
32 :
33 : using namespace std::literals;
34 :
35 : namespace
36 : {
37 :
38 : using cpp_includes = std::set< std::string >;
39 :
40 2252 : void dump_comment( std::ostream & stream, const proto_comment & comment )
41 : {
42 4779 : for( const auto & comm : comment.comments )
43 : {
44 2527 : if( comm.starts_with( "//[[" ) )
45 : {
46 : //- ignore options in comments
47 137 : continue;
48 : }
49 :
50 2390 : stream << comm;
51 2390 : if( comm.back( ) != '\n' )
52 : {
53 2390 : stream << '\n';
54 : }
55 : }
56 2252 : }
57 :
58 88 : auto trim_include( std::string_view str ) -> std::string
59 : {
60 88 : auto p_begin = str.data( );
61 88 : auto p_end = str.data( ) + str.size( );
62 88 : while( p_begin < p_end && isspace( *p_begin ) )
63 : {
64 0 : p_begin++;
65 : }
66 :
67 88 : while( p_begin < p_end && isspace( p_end[ -1 ] ) )
68 : {
69 0 : p_end--;
70 : }
71 :
72 88 : if( p_begin == p_end )
73 : {
74 0 : return { };
75 : }
76 :
77 88 : auto add_prefix = *p_begin != '"' && *p_begin != '<';
78 88 : auto add_postfix = p_end[ -1 ] != '"' && p_end[ -1 ] != '>';
79 :
80 88 : if( add_prefix || add_postfix )
81 : {
82 0 : return '"' + std::string( str ) + '"';
83 : }
84 176 : return std::string( str );
85 : }
86 :
87 12 : void dump_includes( std::ostream & stream, const cpp_includes & includes )
88 : {
89 100 : for( auto & include : includes )
90 : {
91 88 : auto file = trim_include( include );
92 88 : if( !file.empty( ) )
93 : {
94 88 : stream << "#include " << file << "\n";
95 : }
96 88 : }
97 12 : stream << "\n";
98 12 : }
99 :
100 316 : auto contains_map( const proto_messages & messages ) -> bool
101 : {
102 619 : for( const auto & message : messages )
103 : {
104 306 : if( !message.maps.empty( ) )
105 : {
106 3 : return true;
107 : }
108 :
109 304 : if( contains_map( message.messages ) )
110 : {
111 1 : return true;
112 : }
113 : }
114 313 : return false;
115 : }
116 :
117 380 : auto contains_oneof( const proto_messages & messages ) -> bool
118 : {
119 745 : for( const auto & message : messages )
120 : {
121 371 : if( !message.oneofs.empty( ) )
122 : {
123 6 : return true;
124 : }
125 :
126 368 : if( contains_oneof( message.messages ) )
127 : {
128 3 : return true;
129 : }
130 : }
131 374 : return false;
132 : }
133 :
134 12 : void get_std_includes( cpp_includes & includes, const proto_file & file )
135 : {
136 24 : includes.insert( "<spb/json.hpp>" );
137 24 : includes.insert( "<spb/pb.hpp>" );
138 24 : includes.insert( "<cstdint>" );
139 12 : includes.insert( "<cstddef>" );
140 :
141 12 : if( contains_map( file.package.messages ) )
142 : {
143 4 : includes.insert( "<map>" );
144 : }
145 12 : if( contains_oneof( file.package.messages ) )
146 : {
147 6 : includes.insert( "<variant>" );
148 : }
149 12 : }
150 :
151 1313 : void get_include_from_options( cpp_includes & includes, const proto_options & options,
152 : const proto_options & message_options,
153 : const proto_options & file_options, std::string_view option_include )
154 : {
155 1313 : if( auto include = options.find( option_include ); include != options.end( ) )
156 : {
157 2 : includes.insert( std::string( include->second ) );
158 2 : return;
159 : }
160 1311 : if( auto include = message_options.find( option_include ); include != message_options.end( ) )
161 : {
162 0 : includes.insert( std::string( include->second ) );
163 0 : return;
164 : }
165 1311 : if( auto include = file_options.find( option_include ); include != file_options.end( ) )
166 : {
167 1311 : includes.insert( std::string( include->second ) );
168 1311 : return;
169 : }
170 : }
171 :
172 1318 : void get_includes_from_field( cpp_includes & includes, const proto_field & field,
173 : const proto_message & message, const proto_file & file )
174 : {
175 1318 : if( field.label == proto_field::Label::OPTIONAL )
176 : {
177 840 : get_include_from_options( includes, field.options, message.options, file.options,
178 : option_optional_include );
179 : }
180 1318 : if( field.label == proto_field::Label::REPEATED )
181 : {
182 243 : get_include_from_options( includes, field.options, message.options, file.options,
183 : option_repeated_include );
184 : }
185 1318 : if( field.label == proto_field::Label::PTR )
186 : {
187 4 : get_include_from_options( includes, field.options, message.options, file.options,
188 : option_pointer_include );
189 : }
190 1318 : if( field.type == proto_field::Type::STRING )
191 : {
192 204 : get_include_from_options( includes, field.options, message.options, file.options,
193 : option_string_include );
194 : }
195 1318 : if( field.type == proto_field::Type::BYTES )
196 : {
197 22 : get_include_from_options( includes, field.options, message.options, file.options,
198 : option_bytes_include );
199 : }
200 1318 : }
201 :
202 460 : void get_message_includes( cpp_includes & includes, const proto_message & message,
203 : const proto_file & file )
204 : {
205 1663 : for( const auto & field : message.fields )
206 : {
207 1203 : get_includes_from_field( includes, field, message, file );
208 : }
209 :
210 464 : for( const auto & oneof : message.oneofs )
211 : {
212 15 : for( const auto & field : oneof.fields )
213 : {
214 11 : get_includes_from_field( includes, field, message, file );
215 : }
216 : }
217 :
218 512 : for( const auto & map : message.maps )
219 : {
220 52 : get_includes_from_field( includes, map.key, message, file );
221 52 : get_includes_from_field( includes, map.value, message, file );
222 : }
223 :
224 908 : for( const auto & m : message.messages )
225 : {
226 448 : get_message_includes( includes, m, file );
227 : }
228 460 : }
229 :
230 12 : void get_user_includes( cpp_includes & includes, const proto_file & file )
231 : {
232 12 : get_message_includes( includes, file.package, file );
233 12 : }
234 :
235 12 : void get_imports( cpp_includes & includes, const proto_file & file )
236 : {
237 14 : for( const auto & import : file.file_imports )
238 : {
239 2 : includes.insert( "\"" + cpp_file_name_from_proto( import.path, ".pb.h" ).string( ) + "\"" );
240 : }
241 12 : }
242 :
243 12 : void dump_pragma( std::ostream & stream, const proto_file & )
244 : {
245 12 : stream << "#pragma once\n\n";
246 12 : }
247 :
248 12 : void dump_syntax( std::ostream & stream, const proto_file & file )
249 : {
250 12 : dump_comment( stream, file.syntax.comments );
251 12 : }
252 :
253 42 : auto type_literal_suffix( proto_field::Type type ) -> std::string_view
254 : {
255 : static constexpr auto type_map =
256 : std::array< std::pair< proto_field::Type, std::string_view >, 12 >{ {
257 : { proto_field::Type::INT64, "LL" },
258 : { proto_field::Type::UINT32, "U" },
259 : { proto_field::Type::UINT64, "ULL" },
260 : { proto_field::Type::SINT64, "LL" },
261 : { proto_field::Type::FIXED32, "U" },
262 : { proto_field::Type::FIXED64, "ULL" },
263 : { proto_field::Type::SFIXED64, "LL" },
264 : { proto_field::Type::FLOAT, "F" },
265 : } };
266 :
267 524 : for( auto [ proto_type, suffix ] : type_map )
268 : {
269 484 : if( type == proto_type )
270 : {
271 2 : return suffix;
272 : }
273 : }
274 :
275 40 : return { };
276 : }
277 :
278 1203 : auto get_field_bits( const proto_field & field ) -> std::string_view
279 : {
280 1203 : if( auto p_name = field.options.find( option_field_type ); p_name != field.options.end( ) )
281 : {
282 114 : auto bitfield = p_name->second;
283 114 : if( auto index = bitfield.find( ':' ); index != std::string_view::npos )
284 : {
285 50 : const_cast< proto_field & >( field ).bit_field = bitfield.substr( index + 1 );
286 50 : return bitfield.substr( index );
287 : }
288 : }
289 1153 : return { };
290 : }
291 :
292 1198 : auto get_container_type( const proto_options & options, const proto_options & message_options,
293 : const proto_options & file_options, std::string_view option,
294 : std::string_view ctype, std::string_view default_type = { } )
295 : -> std::string
296 : {
297 1198 : if( auto p_name = options.find( option ); p_name != options.end( ) )
298 : {
299 6 : return replace( p_name->second, "$", ctype );
300 : }
301 :
302 1192 : if( auto p_name = message_options.find( option ); p_name != message_options.end( ) )
303 : {
304 0 : return replace( p_name->second, "$", ctype );
305 : }
306 :
307 1192 : if( auto p_name = file_options.find( option ); p_name != file_options.end( ) )
308 : {
309 1192 : return replace( p_name->second, "$", ctype );
310 : }
311 :
312 0 : return replace( default_type, "$", ctype );
313 : }
314 :
315 83 : auto get_enum_type( const proto_file & file, const proto_options & options,
316 : const proto_options & message_options, const proto_options & file_options,
317 : std::string_view default_type ) -> std::string_view
318 : {
319 : static constexpr auto type_map =
320 : std::array< std::pair< std::string_view, std::string_view >, 6 >{ {
321 : { "int8"sv, "int8_t"sv },
322 : { "uint8"sv, "uint8_t"sv },
323 : { "int16"sv, "int16_t"sv },
324 : { "uint16"sv, "uint16_t"sv },
325 : { "int32"sv, "int32_t"sv },
326 : } };
327 :
328 83 : auto ctype_from_pb = [ & ]( std::string_view type )
329 : {
330 385 : for( auto [ proto_type, c_type ] : type_map )
331 : {
332 385 : if( type == proto_type )
333 : {
334 83 : return c_type;
335 : }
336 : }
337 0 : throw_parse_error( file, type, "invalid enum type: " + std::string( type ) );
338 83 : };
339 :
340 83 : if( auto p_name = options.find( option_enum_type ); p_name != options.end( ) )
341 : {
342 15 : return ctype_from_pb( p_name->second );
343 : }
344 :
345 68 : if( auto p_name = message_options.find( option_enum_type ); p_name != message_options.end( ) )
346 : {
347 0 : return ctype_from_pb( p_name->second );
348 : }
349 :
350 68 : if( auto p_name = file_options.find( option_enum_type ); p_name != file_options.end( ) )
351 : {
352 68 : return ctype_from_pb( p_name->second );
353 : }
354 :
355 0 : return default_type;
356 : }
357 :
358 1318 : auto convert_to_ctype( const proto_file & file, const proto_field & field,
359 : const proto_message & message = { } ) -> std::string
360 : {
361 1318 : if( field.bit_type != proto_field::BitType::NONE )
362 : {
363 114 : switch( field.bit_type )
364 : {
365 0 : case proto_field::BitType::NONE:
366 0 : throw_parse_error( file, field.type_name, "invalid type" );
367 24 : case proto_field::BitType::INT8:
368 48 : return "int8_t";
369 24 : case proto_field::BitType::INT16:
370 48 : return "int16_t";
371 12 : case proto_field::BitType::INT32:
372 24 : return "int32_t";
373 6 : case proto_field::BitType::INT64:
374 12 : return "int64_t";
375 18 : case proto_field::BitType::UINT8:
376 36 : return "uint8_t";
377 18 : case proto_field::BitType::UINT16:
378 36 : return "uint16_t";
379 10 : case proto_field::BitType::UINT32:
380 20 : return "uint32_t";
381 2 : case proto_field::BitType::UINT64:
382 4 : return "uint64_t";
383 : }
384 : }
385 :
386 1204 : switch( field.type )
387 : {
388 0 : case proto_field::Type::NONE:
389 0 : throw_parse_error( file, field.type_name, "invalid type" );
390 :
391 204 : case proto_field::Type::STRING:
392 204 : return get_container_type( field.options, message.options, file.options, option_string_type,
393 204 : "char", "std::string" );
394 22 : case proto_field::Type::BYTES:
395 22 : return get_container_type( field.options, message.options, file.options, option_bytes_type,
396 22 : "std::byte", "std::vector<$>" );
397 498 : case proto_field::Type::ENUM:
398 : case proto_field::Type::MESSAGE:
399 498 : return replace( field.type_name, ".", "::" );
400 :
401 8 : case proto_field::Type::FLOAT:
402 16 : return "float";
403 45 : case proto_field::Type::DOUBLE:
404 90 : return "double";
405 63 : case proto_field::Type::BOOL:
406 126 : return "bool";
407 75 : case proto_field::Type::SFIXED32:
408 : case proto_field::Type::INT32:
409 : case proto_field::Type::SINT32:
410 150 : return "int32_t";
411 48 : case proto_field::Type::FIXED32:
412 : case proto_field::Type::UINT32:
413 96 : return "uint32_t";
414 105 : case proto_field::Type::SFIXED64:
415 : case proto_field::Type::INT64:
416 : case proto_field::Type::SINT64:
417 210 : return "int64_t";
418 136 : case proto_field::Type::UINT64:
419 : case proto_field::Type::FIXED64:
420 272 : return "uint64_t";
421 : }
422 :
423 0 : throw_parse_error( file, field.type_name, "invalid type" );
424 : }
425 :
426 1203 : void dump_field_type_and_name( std::ostream & stream, const proto_field & field,
427 : const proto_message & message, const proto_file & file )
428 : {
429 1203 : const auto ctype = convert_to_ctype( file, field, message );
430 :
431 1203 : switch( field.label )
432 : {
433 231 : case proto_field::Label::NONE:
434 231 : stream << ctype << ' ' << field.name << get_field_bits( field );
435 231 : return;
436 725 : case proto_field::Label::OPTIONAL:
437 1450 : stream << get_container_type( field.options, message.options, file.options,
438 725 : option_optional_type, ctype, "std::optional<$>" );
439 725 : break;
440 243 : case proto_field::Label::REPEATED:
441 486 : stream << get_container_type( field.options, message.options, file.options,
442 243 : option_repeated_type, ctype, "std::vector<$>" );
443 243 : break;
444 4 : case proto_field::Label::PTR:
445 8 : stream << get_container_type( field.options, message.options, file.options,
446 4 : option_pointer_type, ctype, "std::unique_ptr<$>" );
447 4 : break;
448 : }
449 972 : if( auto bitfield = get_field_bits( field ); !bitfield.empty( ) )
450 : {
451 0 : throw_parse_error( file, bitfield, "bitfield can be used only with `required` label" );
452 : }
453 972 : stream << ' ' << field.name;
454 1203 : }
455 :
456 438 : void dump_enum_field( std::ostream & stream, const proto_base & field )
457 : {
458 438 : dump_comment( stream, field.comment );
459 :
460 438 : stream << field.name << " = " << field.number << ",\n";
461 438 : }
462 :
463 83 : void dump_enum( std::ostream & stream, const proto_enum & my_enum, const proto_message & message,
464 : const proto_file & file )
465 : {
466 83 : dump_comment( stream, my_enum.comment );
467 :
468 : stream << "enum class " << my_enum.name << " : "
469 166 : << get_enum_type( file, my_enum.options, message.options, file.options, "int32_t" )
470 83 : << "\n{\n";
471 521 : for( const auto & field : my_enum.fields )
472 : {
473 438 : dump_enum_field( stream, field );
474 : }
475 83 : stream << "};\n";
476 83 : }
477 :
478 4 : void dump_message_oneof( std::ostream & stream, const proto_oneof & oneof, const proto_file & file )
479 : {
480 4 : dump_comment( stream, oneof.comment );
481 :
482 4 : auto put_comma = false;
483 4 : stream << "std::variant< ";
484 15 : for( const auto & field : oneof.fields )
485 : {
486 11 : if( put_comma )
487 : {
488 7 : stream << ", ";
489 : }
490 :
491 11 : stream << convert_to_ctype( file, field );
492 11 : put_comma = true;
493 : }
494 4 : stream << " > " << oneof.name << ";\n";
495 15 : }
496 :
497 52 : void dump_message_map( std::ostream & stream, const proto_map & map, const proto_file & file )
498 : {
499 52 : dump_comment( stream, map.comment );
500 :
501 52 : stream << "std::map< " << convert_to_ctype( file, map.key ) << ", "
502 156 : << convert_to_ctype( file, map.value ) << " > ";
503 52 : stream << map.name << ";\n";
504 156 : }
505 :
506 1203 : void dump_default_value( std::ostream & stream, const proto_field & field )
507 : {
508 1203 : if( auto p_index = field.options.find( "default" ); p_index != field.options.end( ) )
509 : {
510 57 : if( field.type == proto_field::Type::ENUM )
511 : {
512 12 : stream << " = " << replace( field.type_name, ".", "::" ) << "::" << p_index->second;
513 : }
514 49 : else if( field.type == proto_field::Type::STRING &&
515 4 : ( p_index->second.size( ) < 2 || p_index->second.front( ) != '"' ||
516 0 : p_index->second.back( ) != '"' ) )
517 : {
518 3 : stream << " = \"" << p_index->second << "\"";
519 : }
520 : else
521 : {
522 42 : stream << " = " << p_index->second << type_literal_suffix( field.type );
523 : }
524 : }
525 1203 : }
526 :
527 1203 : void dump_deprecated_attribute( std::ostream & stream, const proto_field & field )
528 : {
529 1203 : if( auto p_index = field.options.find( "deprecated" );
530 1203 : p_index != field.options.end( ) && p_index->second == "true" )
531 : {
532 9 : stream << "[[deprecated]] ";
533 : }
534 1203 : }
535 :
536 1203 : void dump_message_field( std::ostream & stream, const proto_field & field,
537 : const proto_message & message, const proto_file & file )
538 : {
539 1203 : dump_comment( stream, field.comment );
540 1203 : dump_deprecated_attribute( stream, field );
541 1203 : dump_field_type_and_name( stream, field, message, file );
542 1203 : dump_default_value( stream, field );
543 1203 : stream << ";\n";
544 1203 : }
545 :
546 460 : void dump_forwards( std::ostream & stream, const forwarded_declarations & forwards )
547 : {
548 483 : for( const auto & forward : forwards )
549 : {
550 23 : stream << "struct " << forward << ";\n";
551 : }
552 460 : if( !forwards.empty( ) )
553 : {
554 3 : stream << '\n';
555 : }
556 460 : }
557 :
558 448 : void dump_message( std::ostream & stream, const proto_message & message, const proto_file & file )
559 : {
560 448 : dump_comment( stream, message.comment );
561 :
562 448 : stream << "struct " << message.name << "\n{\n";
563 :
564 448 : dump_forwards( stream, message.forwards );
565 521 : for( const auto & sub_enum : message.enums )
566 : {
567 73 : dump_enum( stream, sub_enum, message, file );
568 : }
569 :
570 548 : for( const auto & sub_message : message.messages )
571 : {
572 100 : dump_message( stream, sub_message, file );
573 : }
574 :
575 1651 : for( const auto & field : message.fields )
576 : {
577 1203 : dump_message_field( stream, field, message, file );
578 : }
579 :
580 500 : for( const auto & map : message.maps )
581 : {
582 52 : dump_message_map( stream, map, file );
583 : }
584 :
585 452 : for( const auto & oneof : message.oneofs )
586 : {
587 4 : dump_message_oneof( stream, oneof, file );
588 : }
589 :
590 : //- TODO: is this used in any way?
591 : // stream << "auto operator == ( const " << message.name << " & ) const noexcept ->
592 : // bool = default;\n"; stream << "auto operator != ( const " << message.name << " &
593 : // ) const noexcept -> bool = default;\n";
594 448 : stream << "};\n";
595 448 : }
596 :
597 12 : void dump_messages( std::ostream & stream, const proto_file & file )
598 : {
599 12 : dump_forwards( stream, file.package.forwards );
600 360 : for( const auto & message : file.package.messages )
601 : {
602 348 : dump_message( stream, message, file );
603 : }
604 12 : }
605 :
606 12 : void dump_enums( std::ostream & stream, const proto_file & file )
607 : {
608 22 : for( const auto & my_enum : file.package.enums )
609 : {
610 10 : dump_enum( stream, my_enum, file.package, file );
611 : }
612 12 : }
613 :
614 12 : void dump_package_begin( std::ostream & stream, const proto_file & file )
615 : {
616 12 : dump_comment( stream, file.package.comment );
617 12 : if( !file.package.name.empty( ) )
618 : {
619 12 : stream << "namespace " << replace( file.package.name, ".", "::" ) << "\n{\n";
620 : }
621 12 : }
622 :
623 12 : void dump_package_end( std::ostream & stream, const proto_file & file )
624 : {
625 12 : if( !file.package.name.empty( ) )
626 : {
627 12 : stream << "}// namespace " << replace( file.package.name, ".", "::" ) << "\n\n";
628 : }
629 12 : }
630 : }// namespace
631 :
632 0 : void throw_parse_error( const proto_file & file, std::string_view at, std::string_view message )
633 : {
634 0 : auto stream = spb::char_stream( file.content );
635 0 : stream.skip_to( at.data( ) );
636 0 : stream.throw_parse_error( message );
637 : }
638 :
639 12 : void dump_cpp_definitions( const proto_file & file, std::ostream & stream )
640 : {
641 12 : auto includes = cpp_includes( );
642 12 : dump_pragma( stream, file );
643 12 : get_imports( includes, file );
644 12 : get_std_includes( includes, file );
645 12 : get_user_includes( includes, file );
646 12 : dump_includes( stream, includes );
647 12 : dump_syntax( stream, file );
648 12 : dump_package_begin( stream, file );
649 12 : dump_enums( stream, file );
650 12 : dump_messages( stream, file );
651 12 : dump_package_end( stream, file );
652 12 : }
653 :
654 1732 : auto replace( std::string_view input, std::string_view what, std::string_view with ) -> std::string
655 : {
656 1732 : auto result = std::string( input );
657 1732 : auto pos = size_t{ };
658 :
659 2786 : while( ( pos = result.find( what, pos ) ) != std::string::npos )
660 : {
661 1054 : result.replace( pos, what.size( ), with );
662 1054 : pos += with.size( );
663 : }
664 :
665 1732 : return result;
666 0 : }
|