// SPDX-License-Identifier: MIT
#include "Object.hh"
#include <cctype>
#include <stdexcept>
#include <vector>
#include "Array.hh"
#include "Boolean.hh"
#include "Number.hh"
#include "String.hh"
using namespace std;
namespace fedem
{
namespace fson
{
// ── Key ──────────────────────────────────────────────────────────────
Key::Key( Form form, string name )
: form( form )
, name( std::move( name ) )
{
if( form == Form::Bare && !isValidName( this->name ) )
throw invalid_argument( "fson::Key: '" + this->name + "' is not a valid bare name" );
}
Key Key::quoted( string name )
{
return Key( Form::Quoted, std::move( name ) );
}
Key Key::bare( string name )
{
return Key( Form::Bare, std::move( name ) );
}
bool Key::isValidName( string const& text ) noexcept
{
if( text.empty( ) || !isalpha( static_cast< unsigned char >( text.front( ) ) ) )
return false;
bool previousUnderscore = false;
for( size_t index = 1U; index < text.size( ); ++index )
{
char const character = text[ index ];
if( character == '_' )
{
if( previousUnderscore )
return false;
previousUnderscore = true;
}
else if( isalnum( static_cast< unsigned char >( character ) ) )
{
previousUnderscore = false;
}
else
{
return false;
}
}
return !previousUnderscore;
}
// ── Member ───────────────────────────────────────────────────────────
Member::Member( Key key, unique_ptr< Value > value, bool disabled )
: key( std::move( key ) )
, disabled( disabled )
, value( std::move( value ) )
{
if( !this->value )
throw invalid_argument( "fson::Member: value must not be null" );
}
Member Member::clone( ) const
{
Member copy( key, value->clone( ), disabled );
copy.leading = leading;
copy.trailing = trailing;
copy.originFile = originFile;
copy.originLocalPath = originLocalPath;
return copy;
}
Key const& Member::getKey( ) const noexcept
{
return key;
}
void Member::setKey( Key key )
{
this->key = std::move( key );
}
bool Member::isDisabled( ) const noexcept
{
return disabled;
}
void Member::setDisabled( bool disabled ) noexcept
{
this->disabled = disabled;
}
Value& Member::getValue( ) noexcept
{
return *value;
}
Value const& Member::getValue( ) const noexcept
{
return *value;
}
void Member::setValue( unique_ptr< Value > value )
{
if( !value )
throw invalid_argument( "fson::Member: value must not be null" );
this->value = std::move( value );
}
CommentList& Member::getLeadingComments( ) noexcept
{
return leading;
}
CommentList const& Member::getLeadingComments( ) const noexcept
{
return leading;
}
optional< Comment >& Member::getTrailingComment( ) noexcept
{
return trailing;
}
optional< Comment > const& Member::getTrailingComment( ) const noexcept
{
return trailing;
}
string const& Member::getOriginFile( ) const noexcept
{
return originFile;
}
string const& Member::getOriginLocalPath( ) const noexcept
{
return originLocalPath;
}
void Member::setOrigin( string sourceFile, string localPath )
{
this->originFile = std::move( sourceFile );
this->originLocalPath = std::move( localPath );
}
bool Member::isMerged( ) const noexcept
{
return !originFile.empty( );
}
// ── Object ───────────────────────────────────────────────────────────
unique_ptr< Value > Object::clone( ) const
{
auto copy = make_unique< Object >( );
copy->members.reserve( members.size( ) );
for( auto const& member : members )
copy->members.push_back( member.clone( ) );
copy->includes.reserve( includes.size( ) );
for( auto const& include : includes )
copy->includes.push_back( include.clone( ) );
copy->entries = entries;
copy->dangling = dangling;
return copy;
}
size_t Object::size( ) const noexcept
{
size_t count = 0U;
for( auto const& member : members )
if( !member.isDisabled( ) )
++count;
return count;
}
size_t Object::findActiveIndex( string const& name ) const noexcept
{
for( size_t index = 0U; index < members.size( ); ++index )
if( !members[ index ].isDisabled( ) && members[ index ].getKey( ).getName( ) == name )
return index;
return NPOS;
}
Value* Object::find( string const& name ) noexcept
{
size_t const index = findActiveIndex( name );
return index == NPOS ? nullptr : &members[ index ].getValue( );
}
Value const* Object::find( string const& name ) const noexcept
{
size_t const index = findActiveIndex( name );
return index == NPOS ? nullptr : &members[ index ].getValue( );
}
Value& Object::at( string const& name )
{
Value* const found = find( name );
if( !found )
throw out_of_range( "fson::Object: no member named '" + name + "'" );
return *found;
}
Value const& Object::at( string const& name ) const
{
Value const* const found = find( name );
if( !found )
throw out_of_range( "fson::Object: no member named '" + name + "'" );
return *found;
}
Value& Object::set( Key key, unique_ptr< Value > value )
{
size_t const index = findActiveIndex( key.getName( ) );
if( index != NPOS )
{
members[ index ].setKey( std::move( key ) );
members[ index ].setValue( std::move( value ) );
// Host-only write policy: overriding a value merged in from an
// %include promotes it to a host member. It now round-trips into
// the host file and reads as local; the included file is untouched.
members[ index ].setOrigin( std::string( ), std::string( ) );
return members[ index ].getValue( );
}
entries.push_back( Entry{ EntryKind::Member, members.size( ) } );
members.emplace_back( std::move( key ), std::move( value ) );
return members.back( ).getValue( );
}
Value& Object::set( string const& name, unique_ptr< Value > value )
{
Key key = Key::isValidName( name ) ? Key::bare( name ) : Key::quoted( name );
return set( std::move( key ), std::move( value ) );
}
bool Object::remove( string const& name ) noexcept
{
size_t const index = findActiveIndex( name );
if( index == NPOS )
return false;
members.erase( members.begin( ) + static_cast< ptrdiff_t >( index ) );
rebuildEntryIndices( EntryKind::Member, index );
return true;
}
bool Object::disable( string const& name ) noexcept
{
size_t const index = findActiveIndex( name );
if( index == NPOS )
return false;
members[ index ].setDisabled( true );
return true;
}
bool Object::enable( string const& name ) noexcept
{
if( findActiveIndex( name ) != NPOS )
return false;
for( auto& member : members )
{
if( member.isDisabled( ) && member.getKey( ).getName( ) == name )
{
member.setDisabled( false );
return true;
}
}
return false;
}
Member& Object::getMember( size_t index )
{
if( index >= members.size( ) )
throw out_of_range( "fson::Object: member index out of range" );
return members[ index ];
}
Member const& Object::getMember( size_t index ) const
{
if( index >= members.size( ) )
throw out_of_range( "fson::Object: member index out of range" );
return members[ index ];
}
Member& Object::appendMember( Member member )
{
entries.push_back( Entry{ EntryKind::Member, members.size( ) } );
members.push_back( std::move( member ) );
return members.back( );
}
void Object::eraseMember( size_t index )
{
if( index >= members.size( ) )
throw out_of_range( "fson::Object: member index out of range" );
members.erase( members.begin( ) + static_cast< ptrdiff_t >( index ) );
rebuildEntryIndices( EntryKind::Member, index );
}
// ── include API ────────────────────────────────────────────────────────
Include& Object::getInclude( size_t index )
{
if( index >= includes.size( ) )
throw out_of_range( "fson::Object: include index out of range" );
return includes[ index ];
}
Include const& Object::getInclude( size_t index ) const
{
if( index >= includes.size( ) )
throw out_of_range( "fson::Object: include index out of range" );
return includes[ index ];
}
Include& Object::appendInclude( Include include )
{
entries.push_back( Entry{ EntryKind::Include, includes.size( ) } );
includes.push_back( std::move( include ) );
return includes.back( );
}
void Object::eraseInclude( size_t index )
{
if( index >= includes.size( ) )
throw out_of_range( "fson::Object: include index out of range" );
includes.erase( includes.begin( ) + static_cast< ptrdiff_t >( index ) );
rebuildEntryIndices( EntryKind::Include, index );
}
// ── entry order ─────────────────────────────────────────────────────────
Object::Entry const& Object::getEntry( size_t position ) const
{
if( position >= entries.size( ) )
throw out_of_range( "fson::Object: entry position out of range" );
return entries[ position ];
}
// After erasing members[removedIndex] (or includes[removedIndex]), drop the
// matching entry and shift down every later entry index of the same kind so
// the entry list keeps pointing at the right elements.
void Object::rebuildEntryIndices( EntryKind kind, size_t removedIndex ) noexcept
{
for( size_t position = 0U; position < entries.size( ); )
{
Entry& entry = entries[ position ];
if( entry.kind == kind && entry.index == removedIndex )
{
entries.erase( entries.begin( ) + static_cast< ptrdiff_t >( position ) );
continue;
}
if( entry.kind == kind && entry.index > removedIndex )
--entry.index;
++position;
}
}
// ── path API ─────────────────────────────────────────────────────────
namespace
{
struct PathSegment
{
bool isIndex;
string key;
size_t index;
};
// "a.b[0].c" → { key a } { key b } { index 0 } { key c }
bool parsePath( string const& path, vector< PathSegment >& segments )
{
if( path.empty( ) )
return false;
size_t position = 0U;
while( position < path.size( ) )
{
// key part — at least one character up to '.' or '['
size_t const start = position;
while( position < path.size( ) &&
path[ position ] != '.' && path[ position ] != '[' )
++position;
if( position == start )
return false; // empty key segment
segments.push_back( { false, path.substr( start, position - start ), 0U } );
// bracket suffixes
while( position < path.size( ) && path[ position ] == '[' )
{
++position;
size_t const digitsStart = position;
while( position < path.size( ) &&
isdigit( static_cast< unsigned char >( path[ position ] ) ) )
++position;
if( position == digitsStart ||
position >= path.size( ) || path[ position ] != ']' )
return false;
segments.push_back(
{ true, string( ),
stoull( path.substr( digitsStart, position - digitsStart ) ) } );
++position; // consume ']'
}
if( position < path.size( ) )
{
if( path[ position ] != '.' )
return false; // e.g. "a[0]b"
++position;
if( position >= path.size( ) )
return false; // trailing '.'
}
}
return true;
}
} // end anonymous namespace
Value* Object::findPath( string const& path ) noexcept
{
vector< PathSegment > segments;
if( !parsePath( path, segments ) )
return nullptr;
Value* current = nullptr;
Object* scope = this;
for( auto const& segment : segments )
{
if( segment.isIndex )
{
Array* const array = current ? current->asArray( ) : nullptr;
if( !array || segment.index >= array->size( ) )
return nullptr;
current = &array->at( segment.index );
}
else
{
if( current )
{
scope = current->asObject( );
if( !scope )
return nullptr;
}
current = scope->find( segment.key );
if( !current )
return nullptr;
}
}
return current;
}
Value const* Object::findPath( string const& path ) const noexcept
{
return const_cast< Object* >( this )->findPath( path );
}
bool Object::containsPath( string const& path ) const noexcept
{
return findPath( path ) != nullptr;
}
Value& Object::atPath( string const& path )
{
Value* const found = findPath( path );
if( !found )
throw out_of_range( "fson::Object: no value at path '" + path + "'" );
return *found;
}
Value const& Object::atPath( string const& path ) const
{
Value const* const found = findPath( path );
if( !found )
throw out_of_range( "fson::Object: no value at path '" + path + "'" );
return *found;
}
Value* Object::setPath( string const& path, unique_ptr< Value > value )
{
vector< PathSegment > segments;
if( !parsePath( path, segments ) || !value )
return nullptr;
Value* current = nullptr;
Object* scope = this;
// Rollback bookkeeping: removing the FIRST created member drops the
// whole created subtree, leaving the document untouched on failure.
Object* createdScope = nullptr;
string createdKey;
auto const rollback = [ &createdScope, &createdKey ]( ) noexcept
{
if( createdScope )
createdScope->remove( createdKey );
};
// walk all but the last segment, creating missing object members
for( size_t position = 0U; position + 1U < segments.size( ); ++position )
{
PathSegment const& segment = segments[ position ];
if( segment.isIndex )
{
Array* const array = current ? current->asArray( ) : nullptr;
if( !array || segment.index >= array->size( ) )
{
rollback( );
return nullptr;
}
current = &array->at( segment.index );
}
else
{
if( current )
{
scope = current->asObject( );
if( !scope )
{
rollback( );
return nullptr;
}
}
Value* next = scope->find( segment.key );
if( !next )
{
next = &scope->set( segment.key, make_unique< Object >( ) );
if( !createdScope )
{
createdScope = scope;
createdKey = segment.key;
}
}
current = next;
}
}
PathSegment const& last = segments.back( );
if( last.isIndex )
{
Array* const array = current ? current->asArray( ) : nullptr;
if( !array || last.index >= array->size( ) )
{
rollback( );
return nullptr;
}
array->getElement( last.index ).setValue( std::move( value ) );
return &array->at( last.index );
}
Object* const target = current ? current->asObject( ) : scope;
if( !target )
{
rollback( );
return nullptr;
}
return &target->set( last.key, std::move( value ) );
}
bool Object::removePath( string const& path ) noexcept
{
vector< PathSegment > segments;
if( !parsePath( path, segments ) )
return false;
PathSegment const& last = segments.back( );
// resolve the parent of the last segment
Value* current = nullptr;
Object* scope = this;
for( size_t position = 0U; position + 1U < segments.size( ); ++position )
{
PathSegment const& segment = segments[ position ];
if( segment.isIndex )
{
Array* const array = current ? current->asArray( ) : nullptr;
if( !array || segment.index >= array->size( ) )
return false;
current = &array->at( segment.index );
}
else
{
if( current )
{
scope = current->asObject( );
if( !scope )
return false;
}
current = scope->find( segment.key );
if( !current )
return false;
}
}
if( last.isIndex )
{
Array* const array = current ? current->asArray( ) : nullptr;
if( !array || last.index >= array->size( ) )
return false;
array->erase( last.index );
return true;
}
Object* const target = current ? current->asObject( ) : scope;
return target ? target->remove( last.key ) : false;
}
// ── provenance API ────────────────────────────────────────────────────
Member const* Object::findOwningMember( string const& path ) const noexcept
{
vector< PathSegment > segments;
if( !parsePath( path, segments ) || segments.empty( ) )
return nullptr;
PathSegment const last = segments.back( );
if( last.isIndex )
return nullptr; // array element has no owning Member
segments.pop_back( );
// Walk to the object that directly owns the final key.
Object const* scope = this;
Value const* current = nullptr;
for( auto const& segment : segments )
{
if( segment.isIndex )
{
Array const* const array = current ? current->asArray( ) : nullptr;
if( !array || segment.index >= array->size( ) )
return nullptr;
current = &array->at( segment.index );
}
else
{
if( current )
{
scope = current->asObject( );
if( !scope )
return nullptr;
}
current = scope->find( segment.key );
if( !current )
return nullptr;
}
}
Object const* owner = current ? current->asObject( ) : scope;
if( !owner )
return nullptr;
size_t const index = owner->findActiveIndex( last.key );
if( index == NPOS )
return nullptr;
return &owner->members[ index ];
}
bool Object::isLocal( string const& path ) const noexcept
{
Member const* const member = findOwningMember( path );
return member && !member->isMerged( );
}
optional< Object::Provenance > Object::provenanceOf( string const& path ) const
{
Member const* const member = findOwningMember( path );
if( !member )
{
// The path may resolve to an array element (no owning member) but
// still exist; report host provenance in that case so a resolvable
// path never yields nullopt.
if( findPath( path ) )
return Provenance{ path, string( ), path, true };
return nullopt;
}
if( member->isMerged( ) )
return Provenance{ path, member->getOriginFile( ),
member->getOriginLocalPath( ), false };
return Provenance{ path, string( ), path, true };
}
std::vector< Object::Provenance > Object::provenanceMap( ) const
{
std::vector< Provenance > result;
for( size_t index = 0U; index < members.size( ); ++index )
{
Member const& member = members[ index ];
if( member.isDisabled( ) )
continue;
string const& name = member.getKey( ).getName( );
if( member.isMerged( ) )
result.push_back( Provenance{ name, member.getOriginFile( ),
member.getOriginLocalPath( ), false } );
else
result.push_back( Provenance{ name, string( ), name, true } );
}
return result;
}
// ── typed convenience getters ────────────────────────────────────────
string Object::getString( string const& path, string const& fallback ) const
{
Value const* const value = findPath( path );
return ( value && value->isString( ) )
? value->asString( )->getValue( ) : fallback;
}
long long Object::getInteger( string const& path, long long fallback ) const noexcept
{
Value const* const value = findPath( path );
return ( value && value->isNumber( ) )
? value->asNumber( )->asInteger( ) : fallback;
}
double Object::getDouble( string const& path, double fallback ) const noexcept
{
Value const* const value = findPath( path );
return ( value && value->isNumber( ) )
? value->asNumber( )->asDouble( ) : fallback;
}
bool Object::getBoolean( string const& path, bool fallback ) const noexcept
{
Value const* const value = findPath( path );
return ( value && value->isBoolean( ) )
? value->asBoolean( )->getValue( ) : fallback;
}
} // end namespace fson
} // end namespace fedem