001package react4j.internal;
002
003import arez.Arez;
004import arez.Observer;
005import arez.spy.ObservableValueInfo;
006import arez.spy.ObserverInfo;
007import grim.annotations.OmitType;
008import java.util.Objects;
009import java.util.stream.Stream;
010import javax.annotation.Nonnull;
011import javax.annotation.Nullable;
012import jsinterop.base.Any;
013import jsinterop.base.Js;
014import jsinterop.base.JsPropertyMap;
015
016/**
017 * Utilities for introspecting the React4j views and runtime.
018 */
019@OmitType( unless = "react4j.store_debug_data_as_state" )
020public final class IntrospectUtil
021{
022  private IntrospectUtil()
023  {
024  }
025
026  /**
027   * Return the value for specified observable.
028   * Exceptions are caught and types are converted to strings using {@link java.lang.Object#toString()}
029   *
030   * @param observableInfo the observable.
031   * @return the value as a string.
032   */
033  @SuppressWarnings( { "UnnecessaryUnboxing", "rawtypes" } )
034  @Nullable
035  public static Object getValue( @Nonnull final ObservableValueInfo observableInfo )
036  {
037    try
038    {
039      if ( Arez.arePropertyIntrospectorsEnabled() && observableInfo.hasAccessor() )
040      {
041        // Consider unwrapping collections and potentially serializing Arez entities so they are presented correctly in DevTools
042        final Object value = observableInfo.getValue();
043        if ( null == value )
044        {
045          return null;
046        }
047        else if ( value instanceof Enum )
048        {
049          return ( (Enum) value ).name();
050        }
051        else if ( value instanceof Integer )
052        {
053          return Js.asAny( ( (Integer) value ).intValue() );
054        }
055        else if ( value instanceof Boolean )
056        {
057          return Js.asAny( ( (Boolean) value ).booleanValue() );
058        }
059        else if ( value instanceof Long )
060        {
061          return Js.asAny( ( (Long) value ).doubleValue() );
062        }
063        else if ( value instanceof Float )
064        {
065          return Js.asAny( ( (Float) value ).doubleValue() );
066        }
067        else if ( value instanceof Short )
068        {
069          return Js.asAny( ( (Short) value ).intValue() );
070        }
071        else if ( value instanceof Byte )
072        {
073          return Js.asAny( ( (Byte) value ).intValue() );
074        }
075        else if ( value instanceof Character )
076        {
077          return value.toString();
078        }
079        else if ( value instanceof Stream )
080        {
081          // Streams are new instances every time so render them as strings to avoid infinite loops.
082          return "<Stream>";
083        }
084        else
085        {
086          return value;
087        }
088      }
089      else
090      {
091        return "?";
092      }
093    }
094    catch ( final Throwable throwable )
095    {
096      return throwable;
097    }
098  }
099
100  /**
101   * For the specified observer, collect all dependencies and record them in data to be emitted as debug data.
102   *
103   * @param observer the observer.
104   * @param data     the target in which to place debug data.
105   */
106  public static void collectDependencyDebugData( @Nonnull final Observer observer,
107                                                 @Nonnull final JsPropertyMap<Object> data )
108  {
109    final ObserverInfo observerInfo = observer.getContext().getSpy().asObserverInfo( observer );
110    observerInfo.getDependencies().forEach( d -> data.set( d.getName(), getValue( d ) ) );
111  }
112
113  /**
114   * Prepare the newState value to be updated given specified current state.
115   * If no changes are required then return false.
116   *
117   * @param newState     the new "state" of the view.
118   * @param currentState the current "state" of the view.
119   * @return true if newState needs to be saved to native view, false otherwise.
120   */
121  public static boolean prepareStateUpdate( @Nonnull final JsPropertyMap<Object> newState,
122                                            @Nullable final JsPropertyMap<Object> currentState )
123  {
124    /*
125     * To determine whether we need to do a state update we do compare each key and value and make sure
126     * they match. In some cases keys can be removed (i.e. a dependency is no longer observed) but as state
127     * updates in react are merges, we need to implement this by putting undefined values into the state.
128     */
129    if ( null != currentState )
130    {
131      boolean[] needsSave = new boolean[ 1 ];
132      currentState.forEach( key -> {
133        if ( !newState.has( key ) )
134        {
135          newState.set( key, Js.undefined() );
136          needsSave[ 0 ] = true;
137        }
138        else
139        {
140          final Any newValue = currentState.getAsAny( key );
141          final Any existingValue = newState.getAsAny( key );
142          if ( !Objects.equals( newValue, existingValue ) )
143          {
144            needsSave[ 0 ] = true;
145          }
146        }
147      } );
148      return needsSave[ 0 ];
149    }
150    else
151    {
152      return true;
153    }
154  }
155}