Code View

fson / source / fson-1.1.0.0 / libs / internal / sdk / fson / Object.cpp
// 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