1f94354caSBartłomiej Bukowskiimport React, { useState, useCallback, useMemo } from 'react';
2f94354caSBartłomiej Bukowskiimport { StyleSheet, View } from 'react-native';
3f94354caSBartłomiej Bukowski
4f94354caSBartłomiej Bukowskiimport ActionButton from './ActionButton';
5f94354caSBartłomiej Bukowskiimport Configurator from './Configurator';
6f94354caSBartłomiej Bukowskiimport Divider from './Divider';
70111d1deSBartłomiej Bukowskiimport FunctionSignature, { generateFunctionSignature } from './FunctionSignature';
84d58621dSBartłomiej Klocekimport Platforms from './Platforms';
9f94354caSBartłomiej Bukowskiimport {
10f94354caSBartłomiej Bukowski  ActionFunction,
11f94354caSBartłomiej Bukowski  ArgumentName,
12f94354caSBartłomiej Bukowski  ConstantParameter,
13f94354caSBartłomiej Bukowski  FunctionArgument,
14f94354caSBartłomiej Bukowski  FunctionParameter,
15f94354caSBartłomiej Bukowski  OnArgumentChangeCallback,
164d58621dSBartłomiej Klocek  Platform,
17f94354caSBartłomiej Bukowski  PrimitiveArgument,
18f94354caSBartłomiej Bukowski  PrimitiveParameter,
19f94354caSBartłomiej Bukowski} from './index.types';
204d58621dSBartłomiej Klocekimport { isCurrentPlatformSupported } from './utils';
21*8a424bebSJames Ideimport HeadingText from '../HeadingText';
22*8a424bebSJames Ideimport MonoTextWithCountdown from '../MonoTextWithCountdown';
23f94354caSBartłomiej Bukowski
24f94354caSBartłomiej Bukowskiconst STRING_TRIM_THRESHOLD = 300;
25f94354caSBartłomiej Bukowski
26f94354caSBartłomiej Bukowskitype Props = {
27f94354caSBartłomiej Bukowski  /**
28f94354caSBartłomiej Bukowski   * Function namespace/scope (e.g. module name). Used in signature rendering.
29f94354caSBartłomiej Bukowski   */
30f94354caSBartłomiej Bukowski  namespace: string;
31f94354caSBartłomiej Bukowski  /**
32f94354caSBartłomiej Bukowski   * Function name. Used in signature rendering.
33f94354caSBartłomiej Bukowski   */
34f94354caSBartłomiej Bukowski  name: string;
35f94354caSBartłomiej Bukowski  /**
364d58621dSBartłomiej Klocek   * Supported platforms. Used in signature rendering and to grey-out unavailable functions.
374d58621dSBartłomiej Klocek   */
384d58621dSBartłomiej Klocek  platforms?: Platform[];
394d58621dSBartłomiej Klocek  /**
40f94354caSBartłomiej Bukowski   * Function-only parameters. Function's arguments are constructed based on these parameters and passed as-is to the actions callbacks.
41f94354caSBartłomiej Bukowski   * These should reflect the actual function signature (type of arguments, default values, order, etc.).
42f94354caSBartłomiej Bukowski   */
43f94354caSBartłomiej Bukowski  parameters?: FunctionParameter[];
44f94354caSBartłomiej Bukowski  /**
45f94354caSBartłomiej Bukowski   * Additional parameters that are directly mappable to the function arguments.
46f94354caSBartłomiej Bukowski   * If you need to add some additional logic to the function call you can do it here.
47f94354caSBartłomiej Bukowski   * The current value for these parameters is passed to the actions' callbacks as the additional arguments.
48f94354caSBartłomiej Bukowski   */
49f94354caSBartłomiej Bukowski  additionalParameters?: PrimitiveParameter[];
50f94354caSBartłomiej Bukowski  /**
51f94354caSBartłomiej Bukowski   * Single action or a list of actions that could be called by the user. Each action would be fetched with the arguments constructed from the parameters.
52f94354caSBartłomiej Bukowski   */
53f94354caSBartłomiej Bukowski  actions: ActionFunction | { name: string; action: ActionFunction }[];
54f94354caSBartłomiej Bukowski  /**
55f94354caSBartłomiej Bukowski   * Rendering function to render some additional components based on the function's result.
56f94354caSBartłomiej Bukowski   */
5710d26b99STomasz Sapeta  renderAdditionalResult?: (result: any) => JSX.Element | void;
58f94354caSBartłomiej Bukowski};
59f94354caSBartłomiej Bukowski
60f94354caSBartłomiej Bukowski/**
61f94354caSBartłomiej Bukowski * Helper type for typing out the function description that is later passed to the `FunctionDemo` component.
62f94354caSBartłomiej Bukowski */
636aedc028Saleqsioexport type FunctionDescription = Omit<Props, 'namespace'>;
64f94354caSBartłomiej Bukowski
650111d1deSBartłomiej Bukowskitype Result =
660111d1deSBartłomiej Bukowski  | {
670111d1deSBartłomiej Bukowski      type: 'none';
680111d1deSBartłomiej Bukowski    }
690111d1deSBartłomiej Bukowski  | {
700111d1deSBartłomiej Bukowski      type: 'error';
710111d1deSBartłomiej Bukowski      error: unknown;
720111d1deSBartłomiej Bukowski    }
730111d1deSBartłomiej Bukowski  | {
740111d1deSBartłomiej Bukowski      type: 'success';
750111d1deSBartłomiej Bukowski      result: unknown;
760111d1deSBartłomiej Bukowski    };
770111d1deSBartłomiej Bukowski
78f94354caSBartłomiej Bukowski/**
79f94354caSBartłomiej Bukowski * FunctionDemo is a component that allows visualizing the function call.
80f94354caSBartłomiej Bukowski * It also allows the function's arguments manipulation and invoking the the function via the actions prop.
81f94354caSBartłomiej Bukowski * Additionally it presents the result of the successful function call.
82f94354caSBartłomiej Bukowski *
83f94354caSBartłomiej Bukowski * @example
84f94354caSBartłomiej Bukowski * ```tsx
85f94354caSBartłomiej Bukowski * const FUNCTION_DESCRIPTION: FunctionDescription = {
86f94354caSBartłomiej Bukowski *   name: 'functionName',
87f94354caSBartłomiej Bukowski *   parameters: [
88f94354caSBartłomiej Bukowski *     { name: 'param1', type: 'string', values: ['value1', 'value2'] },
89f94354caSBartłomiej Bukowski *     ...
90f94354caSBartłomiej Bukowski *   ],
91f94354caSBartłomiej Bukowski *   additionalParameters: [
92f94354caSBartłomiej Bukowski *     { name: 'additionalParameter', type: 'boolean', initial: false },
93f94354caSBartłomiej Bukowski *     ...
94f94354caSBartłomiej Bukowski *   ]
95f94354caSBartłomiej Bukowski *   actions: [
96f94354caSBartłomiej Bukowski *     {
97f94354caSBartłomiej Bukowski *       name: 'actionName',
98f94354caSBartłomiej Bukowski *       action: async (param1: string, ..., additionalParameter: boolean, ...) => {
99f94354caSBartłomiej Bukowski *         ...
100f94354caSBartłomiej Bukowski *         return someObject
101f94354caSBartłomiej Bukowski *       }
102f94354caSBartłomiej Bukowski *     },
103f94354caSBartłomiej Bukowski *     ...
104f94354caSBartłomiej Bukowski *   ]
105f94354caSBartłomiej Bukowski * }
106f94354caSBartłomiej Bukowski *
107f94354caSBartłomiej Bukowski * function DemoComponent() {
108f94354caSBartłomiej Bukowski *   return (
109f94354caSBartłomiej Bukowski *     <FunctionDemo namespace="ModuleName" {...FUNCTION_DESCRIPTION} />
110f94354caSBartłomiej Bukowski *   )
111f94354caSBartłomiej Bukowski * }
112f94354caSBartłomiej Bukowski * ```
113f94354caSBartłomiej Bukowski */
1144d58621dSBartłomiej Klocekexport default function FunctionDemo({ name, platforms = [], ...contentProps }: Props) {
1154d58621dSBartłomiej Klocek  const disabled = !isCurrentPlatformSupported(platforms);
1164d58621dSBartłomiej Klocek
1174d58621dSBartłomiej Klocek  return (
1184d58621dSBartłomiej Klocek    <View style={disabled && styles.demoContainerDisabled}>
1194d58621dSBartłomiej Klocek      <Platforms
1204d58621dSBartłomiej Klocek        platforms={platforms}
1214d58621dSBartłomiej Klocek        style={styles.platformBadge}
1224d58621dSBartłomiej Klocek        textStyle={styles.platformText}
1234d58621dSBartłomiej Klocek      />
1244d58621dSBartłomiej Klocek      <HeadingText style={disabled && styles.headerDisabled}>{name}</HeadingText>
1254d58621dSBartłomiej Klocek      {!disabled && <FunctionDemoContent name={name} {...contentProps} />}
1264d58621dSBartłomiej Klocek    </View>
1274d58621dSBartłomiej Klocek  );
1284d58621dSBartłomiej Klocek}
1294d58621dSBartłomiej Klocek
1304d58621dSBartłomiej Klocekfunction FunctionDemoContent({
131f94354caSBartłomiej Bukowski  namespace,
132f94354caSBartłomiej Bukowski  name,
133f94354caSBartłomiej Bukowski  parameters = [],
134f94354caSBartłomiej Bukowski  actions,
135f94354caSBartłomiej Bukowski  renderAdditionalResult,
136f94354caSBartłomiej Bukowski  additionalParameters = [],
137f94354caSBartłomiej Bukowski}: Props) {
1380111d1deSBartłomiej Bukowski  const [result, setResult] = useState<Result>({ type: 'none' });
139f94354caSBartłomiej Bukowski  const [args, updateArgument] = useArguments(parameters);
140f94354caSBartłomiej Bukowski  const [additionalArgs, updateAdditionalArgs] = useArguments(additionalParameters);
141f94354caSBartłomiej Bukowski  const actionsList = useMemo(
142f94354caSBartłomiej Bukowski    () => (Array.isArray(actions) ? actions : [{ name: 'RUN ▶️', action: actions }]),
143f94354caSBartłomiej Bukowski    [actions]
144f94354caSBartłomiej Bukowski  );
145f94354caSBartłomiej Bukowski
146f94354caSBartłomiej Bukowski  const handlePress = useCallback(
147f94354caSBartłomiej Bukowski    async (action: ActionFunction) => {
148c2fb1d4cSBartłomiej Klocek      // force clear the previous result if exists
1490111d1deSBartłomiej Bukowski      setResult({ type: 'none' });
1500111d1deSBartłomiej Bukowski      try {
151c2fb1d4cSBartłomiej Klocek        const newResult = await action(...args, ...additionalArgs);
1520111d1deSBartłomiej Bukowski        setResult({ type: 'success', result: newResult });
1530111d1deSBartłomiej Bukowski      } catch (e) {
1540111d1deSBartłomiej Bukowski        logError(e, generateFunctionSignature({ namespace, name, parameters, args }));
1550111d1deSBartłomiej Bukowski        setResult({ type: 'error', error: e });
1560111d1deSBartłomiej Bukowski      }
157f94354caSBartłomiej Bukowski    },
158f94354caSBartłomiej Bukowski    [args, additionalArgs]
159f94354caSBartłomiej Bukowski  );
160f94354caSBartłomiej Bukowski
161f94354caSBartłomiej Bukowski  return (
162f94354caSBartłomiej Bukowski    <>
163f94354caSBartłomiej Bukowski      <Configurator parameters={parameters} onChange={updateArgument} value={args} />
164f94354caSBartłomiej Bukowski      {additionalParameters.length > 0 && (
165f94354caSBartłomiej Bukowski        <>
166f94354caSBartłomiej Bukowski          <Divider text="ADDITIONAL PARAMETERS" />
167f94354caSBartłomiej Bukowski          <Configurator
168f94354caSBartłomiej Bukowski            parameters={additionalParameters}
169f94354caSBartłomiej Bukowski            onChange={updateAdditionalArgs}
170f94354caSBartłomiej Bukowski            value={additionalArgs}
171f94354caSBartłomiej Bukowski          />
172f94354caSBartłomiej Bukowski        </>
173f94354caSBartłomiej Bukowski      )}
174f94354caSBartłomiej Bukowski      <View style={styles.container}>
175f94354caSBartłomiej Bukowski        <FunctionSignature namespace={namespace} name={name} parameters={parameters} args={args} />
176f94354caSBartłomiej Bukowski        <View style={styles.buttonsContainer}>
177f94354caSBartłomiej Bukowski          {actionsList.map(({ name, action }) => (
178f94354caSBartłomiej Bukowski            <ActionButton key={name} name={name} action={action} onPress={handlePress} />
179f94354caSBartłomiej Bukowski          ))}
180f94354caSBartłomiej Bukowski        </View>
181f94354caSBartłomiej Bukowski      </View>
1820111d1deSBartłomiej Bukowski      {result.type === 'success' ? (
183f94354caSBartłomiej Bukowski        <>
1840111d1deSBartłomiej Bukowski          <MonoTextWithCountdown onCountdownEnded={() => setResult({ type: 'none' })}>
1850111d1deSBartłomiej Bukowski            {resultToString(result.result)}
186f94354caSBartłomiej Bukowski          </MonoTextWithCountdown>
1870111d1deSBartłomiej Bukowski          {renderAdditionalResult?.(result.result)}
188f94354caSBartłomiej Bukowski        </>
1890111d1deSBartłomiej Bukowski      ) : result.type === 'error' ? (
1900111d1deSBartłomiej Bukowski        <MonoTextWithCountdown
1910111d1deSBartłomiej Bukowski          style={styles.errorResult}
1920111d1deSBartłomiej Bukowski          onCountdownEnded={() => setResult({ type: 'none' })}>
1930111d1deSBartłomiej Bukowski          {errorToString(result.error)}
1940111d1deSBartłomiej Bukowski        </MonoTextWithCountdown>
1950111d1deSBartłomiej Bukowski      ) : null}
196f94354caSBartłomiej Bukowski    </>
197f94354caSBartłomiej Bukowski  );
198f94354caSBartłomiej Bukowski}
199f94354caSBartłomiej Bukowski
2000111d1deSBartłomiej Bukowskifunction logError(e: unknown, functionSignature: string) {
2010111d1deSBartłomiej Bukowski  console.error(`
2020111d1deSBartłomiej Bukowski${e}
2030111d1deSBartłomiej Bukowski
2040111d1deSBartłomiej BukowskiFunction call that failed:
2050111d1deSBartłomiej Bukowski
2060111d1deSBartłomiej Bukowski  ${functionSignature.replace(/\n/g, '\n  ')}
2070111d1deSBartłomiej Bukowski
2080111d1deSBartłomiej Bukowski  `);
2090111d1deSBartłomiej Bukowski}
2100111d1deSBartłomiej Bukowski
211f94354caSBartłomiej Bukowskifunction initialArgumentFromParameter(parameter: PrimitiveParameter | ConstantParameter) {
212f94354caSBartłomiej Bukowski  switch (parameter.type) {
213f94354caSBartłomiej Bukowski    case 'boolean':
214f94354caSBartłomiej Bukowski      return parameter.initial;
215f94354caSBartłomiej Bukowski    case 'string':
216f94354caSBartłomiej Bukowski    case 'number':
217f94354caSBartłomiej Bukowski      return parameter.values[0];
218f94354caSBartłomiej Bukowski    case 'enum':
219f94354caSBartłomiej Bukowski      return parameter.values[0].value;
220f94354caSBartłomiej Bukowski    case 'constant':
221f94354caSBartłomiej Bukowski      return parameter.value;
222f94354caSBartłomiej Bukowski  }
223f94354caSBartłomiej Bukowski}
224f94354caSBartłomiej Bukowski
225f94354caSBartłomiej Bukowskifunction initialArgumentsFromParameters(parameters: FunctionParameter[]) {
226f94354caSBartłomiej Bukowski  return parameters.map((parameter) => {
227f94354caSBartłomiej Bukowski    switch (parameter.type) {
228f94354caSBartłomiej Bukowski      case 'object':
229f94354caSBartłomiej Bukowski        return Object.fromEntries(
230f94354caSBartłomiej Bukowski          parameter.properties.map((property) => {
231f94354caSBartłomiej Bukowski            return [property.name, initialArgumentFromParameter(property)];
232f94354caSBartłomiej Bukowski          })
233f94354caSBartłomiej Bukowski        );
234f94354caSBartłomiej Bukowski      default:
235f94354caSBartłomiej Bukowski        return initialArgumentFromParameter(parameter);
236f94354caSBartłomiej Bukowski    }
237f94354caSBartłomiej Bukowski  });
238f94354caSBartłomiej Bukowski}
239f94354caSBartłomiej Bukowski
240f94354caSBartłomiej Bukowski/**
241f94354caSBartłomiej Bukowski * Hook that handles function arguments' values.
242f94354caSBartłomiej Bukowski * Initial value is constructed based on the description of each parameter.
243f94354caSBartłomiej Bukowski */
244864abfb4STomasz Sapetaexport function useArguments(
245f94354caSBartłomiej Bukowski  parameters: FunctionParameter[]
246f94354caSBartłomiej Bukowski): [FunctionArgument[], OnArgumentChangeCallback] {
247f94354caSBartłomiej Bukowski  const [args, setArgs] = useState(initialArgumentsFromParameters(parameters));
248f94354caSBartłomiej Bukowski  const updateArgument = useCallback(
249f94354caSBartłomiej Bukowski    (name: ArgumentName, newValue: PrimitiveArgument) => {
250f94354caSBartłomiej Bukowski      const parameterIsObject = typeof name === 'object';
251f94354caSBartłomiej Bukowski      const argumentName = parameterIsObject ? name[0] : name;
252f94354caSBartłomiej Bukowski      const argumentIdx = parameters.findIndex((parameter) => parameter.name === argumentName);
253f94354caSBartłomiej Bukowski      setArgs((currentArgs) => {
254f94354caSBartłomiej Bukowski        const newArgs = [...currentArgs];
255f94354caSBartłomiej Bukowski        newArgs[argumentIdx] = parameterIsObject
256f94354caSBartłomiej Bukowski          ? {
257f94354caSBartłomiej Bukowski              ...(currentArgs[argumentIdx] as object),
258f94354caSBartłomiej Bukowski              [name[1]]: newValue,
259f94354caSBartłomiej Bukowski            }
260f94354caSBartłomiej Bukowski          : newValue;
261f94354caSBartłomiej Bukowski        return newArgs;
262f94354caSBartłomiej Bukowski      });
263f94354caSBartłomiej Bukowski    },
264f94354caSBartłomiej Bukowski    [parameters]
265f94354caSBartłomiej Bukowski  );
266f94354caSBartłomiej Bukowski  return [args, updateArgument];
267f94354caSBartłomiej Bukowski}
268f94354caSBartłomiej Bukowski
269f94354caSBartłomiej Bukowskifunction resultToString(result: unknown) {
270f94354caSBartłomiej Bukowski  if (result === null) {
271f94354caSBartłomiej Bukowski    return 'null';
272f94354caSBartłomiej Bukowski  }
273f94354caSBartłomiej Bukowski
274f94354caSBartłomiej Bukowski  if (result === 'undefined') {
275f94354caSBartłomiej Bukowski    return 'undefined';
276f94354caSBartłomiej Bukowski  }
277f94354caSBartłomiej Bukowski
278f94354caSBartłomiej Bukowski  if (typeof result === 'object') {
279f94354caSBartłomiej Bukowski    const trimmedResult = Object.fromEntries(
280f94354caSBartłomiej Bukowski      Object.entries(result).map(([key, value]) => [
281f94354caSBartłomiej Bukowski        key,
282f94354caSBartłomiej Bukowski        typeof value === 'string' && value.length > STRING_TRIM_THRESHOLD
283f94354caSBartłomiej Bukowski          ? `${value.substring(0, STRING_TRIM_THRESHOLD)}...`
284f94354caSBartłomiej Bukowski          : value,
285f94354caSBartłomiej Bukowski      ])
286f94354caSBartłomiej Bukowski    );
287f94354caSBartłomiej Bukowski
288f94354caSBartłomiej Bukowski    return JSON.stringify(trimmedResult, null, 2);
289f94354caSBartłomiej Bukowski  }
290f94354caSBartłomiej Bukowski
291f94354caSBartłomiej Bukowski  return String(result).length > STRING_TRIM_THRESHOLD
292f94354caSBartłomiej Bukowski    ? `${String(result).substring(0, STRING_TRIM_THRESHOLD)}...`
293f94354caSBartłomiej Bukowski    : String(result);
294f94354caSBartłomiej Bukowski}
295f94354caSBartłomiej Bukowski
2960111d1deSBartłomiej Bukowskifunction errorToString(error: unknown) {
2970111d1deSBartłomiej Bukowski  if (error instanceof Error) {
2980111d1deSBartłomiej Bukowski    return `${error.name}: ${error.message}`;
2990111d1deSBartłomiej Bukowski  }
3000111d1deSBartłomiej Bukowski  return String(error);
3010111d1deSBartłomiej Bukowski}
3020111d1deSBartłomiej Bukowski
303f94354caSBartłomiej Bukowskiconst styles = StyleSheet.create({
304f94354caSBartłomiej Bukowski  container: {
305f94354caSBartłomiej Bukowski    position: 'relative',
306f94354caSBartłomiej Bukowski    paddingBottom: 20,
307f94354caSBartłomiej Bukowski  },
308f94354caSBartłomiej Bukowski  buttonsContainer: {
309f94354caSBartłomiej Bukowski    position: 'absolute',
310f94354caSBartłomiej Bukowski    right: 0,
311f94354caSBartłomiej Bukowski    bottom: 3,
312f94354caSBartłomiej Bukowski    flexDirection: 'row',
313f94354caSBartłomiej Bukowski  },
3144d58621dSBartłomiej Klocek  platformBadge: {
3154d58621dSBartłomiej Klocek    position: 'absolute',
3164d58621dSBartłomiej Klocek    top: 5,
3174d58621dSBartłomiej Klocek  },
3184d58621dSBartłomiej Klocek  platformText: {
3194d58621dSBartłomiej Klocek    fontSize: 10,
3204d58621dSBartłomiej Klocek  },
3214d58621dSBartłomiej Klocek  headerDisabled: {
3224d58621dSBartłomiej Klocek    textDecorationLine: 'line-through',
3234d58621dSBartłomiej Klocek    color: '#999',
3244d58621dSBartłomiej Klocek  },
3254d58621dSBartłomiej Klocek  demoContainerDisabled: {
3264d58621dSBartłomiej Klocek    marginBottom: 10,
3274d58621dSBartłomiej Klocek  },
3280111d1deSBartłomiej Bukowski  errorResult: {
3290111d1deSBartłomiej Bukowski    borderColor: 'red',
3300111d1deSBartłomiej Bukowski  },
331f94354caSBartłomiej Bukowski});
332