// SPDX-License-Identifier: MIT
#include "MString.hh"
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iterator>
#include <ostream>
#include "MException.hh"
#include "MIncludes.hh"
#include "MMessage.hh"
#include "MNamespace.hh"
#include "MText.hh"
#include "MUsing.hh"
using namespace std;
#ifndef MSTRING_VERSION
#define MSTRING_VERSION "unknown"
#endif
namespace
{
bool Any( const Module &module, bool ( *predicate )( const MessageSettings & ) )
{
return any_of( module.Messages.begin( ), module.Messages.end( ),
[predicate]( const Message &message ) { return predicate( *message.Settings ); } );
}
bool HasStreamable( const MessageSettings &s ) { return s._Streamable._Active; }
bool HasString( const MessageSettings &s ) { return s._String._Active; }
bool HasError( const MessageSettings &s ) { return s._Error._Active; }
bool HasLog( const MessageSettings &s ) { return s._Log._Active; }
bool HasException( const MessageSettings &s ) { return s._Exception._Active || s._Throw._Active; }
/// Standard headers the declarations need, then the user's `$INCLUDE`s.
vector<string> InterfaceIncludes( const Module &module )
{
vector<string> includes;
bool const classes = Any( module, HasStreamable ) || Any( module, HasException );
if ( Any( module, HasStreamable ) )
includes.push_back( "<iosfwd>" );
includes.push_back( "<locale>" );
if ( Any( module, HasString ) || Any( module, HasException ) )
includes.push_back( "<string>" );
if ( classes )
includes.push_back( "<type_traits>" );
if ( Any( module, HasException ) )
includes.push_back( "<exception>" );
if ( Any( module, HasLog ) )
includes.push_back( "<syslog.h>" );
for ( const string &include : module.Includes )
{
if ( find( includes.begin( ), includes.end( ), include ) == includes.end( ) )
includes.push_back( include );
}
return includes;
}
string GuardMacro( const string &fileName )
{
string macro = "MSTRING_";
for ( char const c : fileName )
{
macro += isalnum( static_cast<unsigned char>( c ) )
? static_cast<char>( toupper( static_cast<unsigned char>( c ) ) )
: '_';
}
return macro;
}
void Banner( ostream &stream, const string &fileName, const string &input )
{
stream << "// " << fileName << endl
<< "//" << endl
<< "// Generated by mstring " << MSTRING_VERSION;
if ( ! input.empty( ) )
{
stream << " from " << input;
}
stream << ". Do not edit: edit the message file" << endl
<< "// and run mstring again." << endl
<< endl;
}
/// Includes, namespaces, `$USING`s and all declarations.
void WriteDeclarations( ostream &stream, const Module &module )
{
string indent;
vector<string> const includes = InterfaceIncludes( module );
for_each( includes.begin( ), includes.end( ), IncludeFileWriter( stream, indent ) );
stream << endl;
for_each( module.Namespaces.begin( ), module.Namespaces.end( ),
NamespaceHeaderWriter( stream, indent ) );
if ( ! module.Usings.empty( ) )
{
// Inside the namespace: the parameter types in the declarations may
// rely on them, and so they do not leak into every includer's scope.
for_each( module.Usings.begin( ), module.Usings.end( ), ImportedEntityWriter( stream, indent ) );
stream << endl;
}
for_each( module.Messages.begin( ), module.Messages.end( ), MessageHeaderWriter( stream, indent ) );
for_each( module.Namespaces.rbegin( ), module.Namespaces.rend( ),
NamespaceTailWriter( stream, indent ) );
}
}
void ValidateModule( const Module &module )
{
if ( module.Inline && ! module.HeaderExtension )
{
throw MException( "$MODULE " + module.Name + ": INLINE puts the definitions into the "
"header, it cannot be combined with NOHEADER" );
}
for ( const Message &message : module.Messages )
{
const MessageSettings &settings = *message.Settings;
if ( ( settings._Exception._Active || settings._Throw._Active ) &&
! settings._Exception._Override.empty( ) && settings._Exception._Parent.empty( ) )
{
throw MException( "message '" + message.Name + "': $EXCEPTION OVERRIDE " +
settings._Exception._Override + " overrides a method of the base class, "
"but the exception has none (INHERITED)" );
}
const pair<const char *, const FunctionConcept *> concepts[] =
{
{ "$STRING", &settings._String }, { "$ERROR", &settings._Error },
{ "$THROW", &settings._Throw }, { "$SYSLOG", &settings._Log },
};
for ( auto const &[directive, concept] : concepts )
{
if ( concept->_Active && concept->_Type == MEMBER_AS && ! settings._Streamable._Active )
{
throw MException( "message '" + message.Name + "': " + directive + " MEMBER AS " +
concept->_Naming + " makes a member of the streamable class, "
"but $STREAMABLE is not enabled" );
}
}
}
}
string HeaderFileName( const Module &module )
{
if ( ! module.HeaderExtension )
{
return "";
}
return module.Name + '.' + *module.HeaderExtension;
}
string SourceFileName( const Module &module )
{
if ( ! module.SourceExtension )
{
return "";
}
return module.Name + '.' + *module.SourceExtension;
}
void WriteCppInterface( ostream &stream, const Module &module, const string &input )
{
Mode.exportMacro = module.Export;
string const fileName = HeaderFileName( module );
string const guard = GuardMacro( fileName );
Banner( stream, fileName, input );
stream << "#ifndef " << guard << endl
<< "#define " << guard << endl
<< endl;
WriteDeclarations( stream, module );
if ( module.Inline )
{
stream << endl
<< "// Definitions ($MODULE ... INLINE)" << endl
<< endl;
WriteDefinitions( stream, module );
}
stream << endl
<< "#endif" << endl;
}
void WriteCppImplementation( ostream &stream, const Module &module, const string &input )
{
Mode.exportMacro = module.Export;
string const header = HeaderFileName( module );
Banner( stream, SourceFileName( module ), input );
if ( ! header.empty( ) )
{
stream << "#include \"" << header << '"' << endl
<< endl;
}
else
{
WriteDeclarations( stream, module );
stream << endl;
}
WriteDefinitions( stream, module );
}
void WriteDefinitions( ostream &stream, const Module &module )
{
string indent;
Mode.inlineDefinitions = module.Inline;
stream << "#include <sstream>" << endl
<< "#include <string>" << endl;
bool const builders = any_of( module.Messages.begin( ), module.Messages.end( ), NeedsBuilder );
if ( builders )
{
stream << "#include <charconv>" << endl
<< "#include <locale>" << endl
<< "#include <string_view>" << endl
<< "#include <type_traits>" << endl;
}
if ( Any( module, HasError ) )
{
stream << "#include <cstdlib>" << endl
<< "#include <iostream>" << endl;
}
stream << endl;
bool const languages = any_of( module.Messages.begin( ), module.Messages.end( ),
[]( const Message &message ) { return message.Locales.size( ) > 1; } );
if ( languages )
{
WriteLocaleMatcher( stream );
}
if ( builders )
{
WriteAppendHelper( stream );
}
for_each( module.Namespaces.begin( ), module.Namespaces.end( ),
NamespaceHeaderWriter( stream, indent ) );
for_each( module.Messages.begin( ), module.Messages.end( ), MessageCodeWriter( stream, indent ) );
for_each( module.Namespaces.rbegin( ), module.Namespaces.rend( ),
NamespaceTailWriter( stream, indent ) );
}
bool WriteIfChanged( const string &path, const string &content )
{
{
ifstream existing( path, ios::binary );
if ( existing )
{
string const old( ( istreambuf_iterator<char>( existing ) ), istreambuf_iterator<char>( ) );
if ( old == content )
{
return false;
}
}
}
ofstream output( path, ios::binary | ios::trunc );
output << content;
output.close( );
if ( ! output )
{
throw MException( "cannot write '" + path + "'" );
}
return true;
}