2023-10-19 16:19:09 +02:00
/*
2024-02-11 23:09:01 +02:00
* JsonUtils . cpp , part of VCMI engine
2023-10-19 16:19:09 +02:00
*
* Authors : listed in file AUTHORS in main folder
*
* License : GNU General Public License v2 .0 or later
* Full text of license available in license . txt file , in main folder
*
*/
# include "StdInc.h"
2024-02-11 23:09:01 +02:00
# include "JsonUtils.h"
# include "JsonValidator.h"
# include "../filesystem/Filesystem.h"
2023-10-19 16:19:09 +02:00
2024-02-14 20:45:14 +02:00
VCMI_LIB_USING_NAMESPACE
2024-02-14 13:21:38 +02:00
2023-10-19 16:19:09 +02:00
static const JsonNode nullNode ;
static JsonNode getDefaultValue ( const JsonNode & schema , std : : string fieldName )
{
const JsonNode & fieldProps = schema [ " properties " ] [ fieldName ] ;
# if defined(VCMI_IOS)
if ( ! fieldProps [ " defaultIOS " ] . isNull ( ) )
return fieldProps [ " defaultIOS " ] ;
# elif defined(VCMI_ANDROID)
if ( ! fieldProps [ " defaultAndroid " ] . isNull ( ) )
return fieldProps [ " defaultAndroid " ] ;
2024-01-14 14:29:13 +02:00
# elif defined(VCMI_WINDOWS)
if ( ! fieldProps [ " defaultWindows " ] . isNull ( ) )
return fieldProps [ " defaultWindows " ] ;
# endif
# if !defined(VCMI_MOBILE)
2023-10-19 16:19:09 +02:00
if ( ! fieldProps [ " defaultDesktop " ] . isNull ( ) )
return fieldProps [ " defaultDesktop " ] ;
# endif
return fieldProps [ " default " ] ;
}
static void eraseOptionalNodes ( JsonNode & node , const JsonNode & schema )
{
assert ( schema [ " type " ] . String ( ) = = " object " ) ;
std : : set < std : : string > foundEntries ;
for ( const auto & entry : schema [ " required " ] . Vector ( ) )
foundEntries . insert ( entry . String ( ) ) ;
2024-02-14 20:35:58 +02:00
vstd : : erase_if ( node . Struct ( ) , [ & foundEntries ] ( const auto & structEntry ) {
return ! vstd : : contains ( foundEntries , structEntry . first ) ;
2023-10-19 16:19:09 +02:00
} ) ;
}
static void minimizeNode ( JsonNode & node , const JsonNode & schema )
{
if ( schema [ " type " ] . String ( ) ! = " object " )
return ;
for ( const auto & entry : schema [ " required " ] . Vector ( ) )
{
const std : : string & name = entry . String ( ) ;
minimizeNode ( node [ name ] , schema [ " properties " ] [ name ] ) ;
if ( vstd : : contains ( node . Struct ( ) , name ) & & node [ name ] = = getDefaultValue ( schema , name ) )
node . Struct ( ) . erase ( name ) ;
}
eraseOptionalNodes ( node , schema ) ;
}
static void maximizeNode ( JsonNode & node , const JsonNode & schema )
{
// "required" entry can only be found in object/struct
if ( schema [ " type " ] . String ( ) ! = " object " )
return ;
// check all required entries that have default version
for ( const auto & entry : schema [ " required " ] . Vector ( ) )
{
const std : : string & name = entry . String ( ) ;
if ( node [ name ] . isNull ( ) & & ! getDefaultValue ( schema , name ) . isNull ( ) )
node [ name ] = getDefaultValue ( schema , name ) ;
maximizeNode ( node [ name ] , schema [ " properties " ] [ name ] ) ;
}
eraseOptionalNodes ( node , schema ) ;
}
2024-02-14 20:45:14 +02:00
VCMI_LIB_NAMESPACE_BEGIN
2023-10-19 16:19:09 +02:00
void JsonUtils : : minimize ( JsonNode & node , const std : : string & schemaName )
{
minimizeNode ( node , getSchema ( schemaName ) ) ;
}
void JsonUtils : : maximize ( JsonNode & node , const std : : string & schemaName )
{
maximizeNode ( node , getSchema ( schemaName ) ) ;
}
bool JsonUtils : : validate ( const JsonNode & node , const std : : string & schemaName , const std : : string & dataName )
{
2024-02-19 17:46:26 +02:00
JsonValidator validator ;
std : : string log = validator . check ( schemaName , node ) ;
2023-10-19 16:19:09 +02:00
if ( ! log . empty ( ) )
{
logMod - > warn ( " Data in %s is invalid! " , dataName ) ;
logMod - > warn ( log ) ;
2024-02-12 01:22:16 +02:00
logMod - > trace ( " %s json: %s " , dataName , node . toCompactString ( ) ) ;
2023-10-19 16:19:09 +02:00
}
return log . empty ( ) ;
}
const JsonNode & getSchemaByName ( const std : : string & name )
{
// cached schemas to avoid loading json data multiple times
static std : : map < std : : string , JsonNode > loadedSchemas ;
if ( vstd : : contains ( loadedSchemas , name ) )
return loadedSchemas [ name ] ;
auto filename = JsonPath : : builtin ( " config/schemas/ " + name ) ;
if ( CResourceHandler : : get ( ) - > existsResource ( filename ) )
{
loadedSchemas [ name ] = JsonNode ( filename ) ;
return loadedSchemas [ name ] ;
}
logMod - > error ( " Error: missing schema with name %s! " , name ) ;
assert ( 0 ) ;
return nullNode ;
}
const JsonNode & JsonUtils : : getSchema ( const std : : string & URI )
{
size_t posColon = URI . find ( ' : ' ) ;
size_t posHash = URI . find ( ' # ' ) ;
std : : string filename ;
if ( posColon = = std : : string : : npos )
{
filename = URI . substr ( 0 , posHash ) ;
}
else
{
std : : string protocolName = URI . substr ( 0 , posColon ) ;
filename = URI . substr ( posColon + 1 , posHash - posColon - 1 ) + " .json " ;
if ( protocolName ! = " vcmi " )
{
logMod - > error ( " Error: unsupported URI protocol for schema: %s " , URI ) ;
return nullNode ;
}
}
// check if json pointer if present (section after hash in string)
if ( posHash = = std : : string : : npos | | posHash = = URI . size ( ) - 1 )
{
auto const & result = getSchemaByName ( filename ) ;
if ( result . isNull ( ) )
logMod - > error ( " Error: missing schema %s " , URI ) ;
return result ;
}
else
{
auto const & result = getSchemaByName ( filename ) . resolvePointer ( URI . substr ( posHash + 1 ) ) ;
if ( result . isNull ( ) )
logMod - > error ( " Error: missing schema %s " , URI ) ;
return result ;
}
}
void JsonUtils : : merge ( JsonNode & dest , JsonNode & source , bool ignoreOverride , bool copyMeta )
{
if ( dest . getType ( ) = = JsonNode : : JsonType : : DATA_NULL )
{
std : : swap ( dest , source ) ;
return ;
}
switch ( source . getType ( ) )
{
case JsonNode : : JsonType : : DATA_NULL :
{
dest . clear ( ) ;
break ;
}
case JsonNode : : JsonType : : DATA_BOOL :
case JsonNode : : JsonType : : DATA_FLOAT :
case JsonNode : : JsonType : : DATA_INTEGER :
case JsonNode : : JsonType : : DATA_STRING :
case JsonNode : : JsonType : : DATA_VECTOR :
{
std : : swap ( dest , source ) ;
break ;
}
case JsonNode : : JsonType : : DATA_STRUCT :
{
2024-02-13 15:20:08 +02:00
if ( ! ignoreOverride & & source . getOverrideFlag ( ) )
2023-10-19 16:19:09 +02:00
{
std : : swap ( dest , source ) ;
}
else
{
if ( copyMeta )
2024-02-13 14:34:16 +02:00
dest . setModScope ( source . getModScope ( ) , false ) ;
2023-10-19 16:19:09 +02:00
2025-03-27 17:12:37 +00:00
if ( dest . isStruct ( ) )
{
//recursively merge all entries from struct
for ( auto & node : source . Struct ( ) )
merge ( dest [ node . first ] , node . second , ignoreOverride ) ;
break ;
}
if ( dest . isVector ( ) )
{
auto getIndexSafe = [ & dest ] ( const std : : string & keyName ) - > std : : optional < int >
{
try {
int index = std : : stoi ( keyName ) ;
2025-03-30 17:18:47 +03:00
if ( index < = 0 | | index > dest . Vector ( ) . size ( ) )
2025-03-27 17:12:37 +00:00
throw std : : out_of_range ( " dummy " ) ;
2025-03-30 17:18:47 +03:00
return index - 1 ; // 1-based index -> 0-based index
2025-03-27 17:12:37 +00:00
}
catch ( const std : : invalid_argument & )
{
logMod - > warn ( " Failed to interpret key '%s' when replacing individual items in array. Expected 'appendItem', 'appendItems', 'modify@NUM' or 'insert@NUM " , keyName ) ;
return std : : nullopt ;
}
catch ( const std : : out_of_range & )
{
2025-03-30 17:18:47 +03:00
logMod - > warn ( " Failed to replace index when replacing individual items in array. Value '%s' does not exists in targeted array of %d items " , keyName , dest . Vector ( ) . size ( ) ) ;
2025-03-27 17:12:37 +00:00
return std : : nullopt ;
}
} ;
for ( auto & node : source . Struct ( ) )
{
if ( node . first = = " append " )
{
dest . Vector ( ) . push_back ( std : : move ( node . second ) ) ;
}
else if ( node . first = = " appendItems " )
{
assert ( node . second . isVector ( ) ) ;
std : : move ( dest . Vector ( ) . begin ( ) , dest . Vector ( ) . end ( ) , std : : back_inserter ( dest . Vector ( ) ) ) ;
}
else if ( boost : : algorithm : : starts_with ( node . first , " insert@ " ) )
{
constexpr int numberPosition = std : : char_traits < char > : : length ( " insert@ " ) ;
auto index = getIndexSafe ( node . first . substr ( numberPosition ) ) ;
if ( index )
dest . Vector ( ) . insert ( dest . Vector ( ) . begin ( ) + index . value ( ) , std : : move ( node . second ) ) ;
}
else if ( boost : : algorithm : : starts_with ( node . first , " modify@ " ) )
{
constexpr int numberPosition = std : : char_traits < char > : : length ( " modify@ " ) ;
auto index = getIndexSafe ( node . first . substr ( numberPosition ) ) ;
if ( index )
merge ( dest . Vector ( ) . at ( index . value ( ) ) , node . second , ignoreOverride ) ;
}
}
break ;
}
assert ( false ) ;
2023-10-19 16:19:09 +02:00
}
}
}
}
void JsonUtils : : mergeCopy ( JsonNode & dest , JsonNode source , bool ignoreOverride , bool copyMeta )
{
// uses copy created in stack to safely merge two nodes
merge ( dest , source , ignoreOverride , copyMeta ) ;
}
void JsonUtils : : inherit ( JsonNode & descendant , const JsonNode & base )
{
JsonNode inheritedNode ( base ) ;
merge ( inheritedNode , descendant , true , true ) ;
2023-10-22 18:36:41 +03:00
std : : swap ( descendant , inheritedNode ) ;
2023-10-19 16:19:09 +02:00
}
2024-09-30 19:26:22 +00:00
JsonNode JsonUtils : : assembleFromFiles ( const JsonNode & files , bool & isValid )
{
if ( files . isVector ( ) )
{
2024-10-30 10:51:02 +00:00
assert ( ! files . getModScope ( ) . empty ( ) ) ;
2024-09-30 19:26:22 +00:00
auto configList = files . convertTo < std : : vector < std : : string > > ( ) ;
2024-10-30 10:51:02 +00:00
JsonNode result = JsonUtils : : assembleFromFiles ( configList , files . getModScope ( ) , isValid ) ;
2024-09-30 19:26:22 +00:00
return result ;
}
else
{
2024-10-10 21:18:43 +00:00
isValid = true ;
2024-09-30 19:26:22 +00:00
return files ;
}
}
JsonNode JsonUtils : : assembleFromFiles ( const JsonNode & files )
{
bool isValid = false ;
return assembleFromFiles ( files , isValid ) ;
}
2023-10-19 16:19:09 +02:00
JsonNode JsonUtils : : assembleFromFiles ( const std : : vector < std : : string > & files )
{
bool isValid = false ;
2024-10-30 10:51:02 +00:00
return assembleFromFiles ( files , " " , isValid ) ;
2023-10-19 16:19:09 +02:00
}
2024-10-30 10:51:02 +00:00
JsonNode JsonUtils : : assembleFromFiles ( const std : : vector < std : : string > & files , std : : string modName , bool & isValid )
2023-10-19 16:19:09 +02:00
{
isValid = true ;
JsonNode result ;
for ( const auto & file : files )
{
2024-05-11 13:10:30 +00:00
JsonPath path = JsonPath : : builtinTODO ( file ) ;
2024-10-30 10:51:02 +00:00
if ( CResourceHandler : : get ( modName ) - > existsResource ( path ) )
2024-05-11 13:10:30 +00:00
{
bool isValidFile = false ;
2024-10-30 10:51:02 +00:00
JsonNode section ( JsonPath : : builtinTODO ( file ) , modName , isValidFile ) ;
2024-05-11 13:10:30 +00:00
merge ( result , section ) ;
isValid | = isValidFile ;
}
else
{
logMod - > error ( " Failed to find file %s " , file ) ;
isValid = false ;
}
2023-10-19 16:19:09 +02:00
}
return result ;
}
JsonNode JsonUtils : : assembleFromFiles ( const std : : string & filename )
{
JsonNode result ;
JsonPath resID = JsonPath : : builtinTODO ( filename ) ;
for ( auto & loader : CResourceHandler : : get ( ) - > getResourcesWithName ( resID ) )
{
2024-02-14 20:35:58 +02:00
auto textData = loader - > load ( resID ) - > readAll ( ) ;
2024-07-17 13:07:57 +02:00
JsonNode section ( reinterpret_cast < std : : byte * > ( textData . first . get ( ) ) , textData . second , resID . getName ( ) ) ;
2023-10-19 16:19:09 +02:00
merge ( result , section ) ;
}
return result ;
}
2024-10-06 15:54:30 +00:00
void JsonUtils : : detectConflicts ( JsonNode & result , const JsonNode & left , const JsonNode & right , const std : : string & keyName )
2024-10-02 18:58:03 +00:00
{
switch ( left . getType ( ) )
{
case JsonNode : : JsonType : : DATA_NULL :
case JsonNode : : JsonType : : DATA_BOOL :
case JsonNode : : JsonType : : DATA_FLOAT :
case JsonNode : : JsonType : : DATA_INTEGER :
case JsonNode : : JsonType : : DATA_STRING :
case JsonNode : : JsonType : : DATA_VECTOR : // NOTE: comparing vectors as whole - since merge will overwrite it in its entirety
{
2024-10-06 15:54:30 +00:00
result [ keyName ] [ left . getModScope ( ) ] = left ;
result [ keyName ] [ right . getModScope ( ) ] = right ;
2024-10-02 18:58:03 +00:00
return ;
}
case JsonNode : : JsonType : : DATA_STRUCT :
{
2024-10-06 15:54:30 +00:00
for ( const auto & node : left . Struct ( ) )
2024-10-02 18:58:03 +00:00
if ( ! right [ node . first ] . isNull ( ) )
2024-10-06 15:54:30 +00:00
detectConflicts ( result , node . second , right [ node . first ] , keyName + " / " + node . first ) ;
2024-10-02 18:58:03 +00:00
}
}
}
2024-02-14 13:21:38 +02:00
VCMI_LIB_NAMESPACE_END