001package io.jstach.jstachio.context;
002
003import java.lang.reflect.Array;
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collection;
007import java.util.Collections;
008import java.util.Iterator;
009import java.util.List;
010import java.util.Map;
011import java.util.concurrent.atomic.AtomicInteger;
012import java.util.stream.Stream;
013import java.util.stream.StreamSupport;
014
015import org.eclipse.jdt.annotation.Nullable;
016
017/**
018 * This interface serves two puproses:
019 *
020 * <ol>
021 * <li>A way to represent the current context stack (see {@link #parent()})
022 * <li>Allow you to simulate JSON/Javscript object node like trees without being coupled
023 * to a particularly JSON lib.
024 * </ol>
025 * The interface simply wraps {@link Map} and {@link Iterable} (and arrays) lazily through
026 * composition but generally cannot wrap other context nodes. If an object is wrapped that
027 * is not a Map or Iterable it becomes a leaf node similar to JSON.
028 * <p>
029 * It is not recommended you use this interface as it avoids much of the type checking
030 * safty of this library as well as increase coupling however it does provide a slightly
031 * better bridge to legacy {@code Map<String,?>} models over using the maps directly.
032 *
033 * @apiNote The parents do not know anything about their children as it is the child that
034 * has reference to the parent.
035 * @author agentgt
036 *
037 */
038public interface ContextNode extends Iterable<ContextNode> {
039
040        /**
041         * Creates the root node which has no name.
042         * @apiNote Unlike the other methods in this class if the passed in object is a
043         * context node it is simply returned if it is a root node otherwise it is rewrapped.
044         * @param o the object to be wrapped. Maybe <code>null</code>.
045         * @return <code>null</code> if the root object is null otherwise a new root node.
046         */
047        public static @Nullable ContextNode ofRoot(@Nullable Object o) {
048                if (o == null) {
049                        return null;
050                }
051                if (o instanceof ContextNode n) {
052                        if (n.parent() != null) {
053                                return ofRoot(n.object());
054                        }
055                        return n;
056                }
057                return new RootContextNode(o);
058
059        }
060
061        /**
062         * Creates a named child node off of this node where the return child nodes parent
063         * will be this node.
064         * @param name the context name.
065         * @param o the object to be wrapped.
066         * @return <code>null</code> if the child object is null otherwise a new child node.
067         * @throws IllegalArgumentException if the input object is a {@link ContextNode}
068         */
069        default @Nullable ContextNode ofChild(String name, @Nullable Object o) throws IllegalArgumentException {
070                if (o == null) {
071                        return null;
072                }
073                if (o instanceof ContextNode) {
074                        throw new IllegalArgumentException("Cannot wrap ContextNode around another ContextNode");
075                }
076                return new NamedContextNode(this, o, name);
077        }
078
079        /**
080         * Creates an indexed child node off of this node where the return child nodes parent
081         * will be this node.
082         * @apiNote there is no checking to see if the same index is reused as the parent
083         * knows nothing of the child.
084         * @param index a numeric index
085         * @param o the object to be wrapped. Maybe <code>null</code>.
086         * @return <code>null</code> if the child object is null otherwise a new child node.
087         * @throws IllegalArgumentException if the input object is a {@link ContextNode}
088         */
089        default @Nullable ContextNode ofChild(int index, @Nullable Object o) {
090                if (o == null) {
091                        return null;
092                }
093                if (o instanceof ContextNode) {
094                        throw new IllegalArgumentException("Cannot wrap ContextNode around another ContextNode");
095                }
096                return new IndexedContextNode(this, o, index);
097        }
098
099        /**
100         * Gets a field from a {@link Map} if ContextNode is wrapping one. This is direct
101         * access (end of a dotted path) and does not check the parents.
102         *
103         * Just like {@link Map} <code>null</code> will be returned if no field is found.
104         * @param field the name of the field
105         * @return a new child node. Maybe <code>null</code>.
106         */
107        default @Nullable ContextNode get(String field) {
108                Object o = object();
109                ContextNode child = null;
110                if (o instanceof Map<?, ?> m) {
111                        child = ofChild(field, m.get(field));
112                }
113                return child;
114        }
115
116        /**
117         * Will search up the tree for a field starting at this nodes children first.
118         * @param field context name (e.g. section name)
119         * @return <code>null</code> if not found otherwise creates a new node from the map or
120         * object containing the field.
121         */
122        default @Nullable ContextNode find(String field) {
123                /*
124                 * In theory we could make a special RenderingContext for ContextNode to go up the
125                 * stack (generated code) but it would probably look similar to the following.
126                 */
127                ContextNode child = get(field);
128                if (child != null) {
129                        return child;
130                }
131                var parent = parent();
132                if (parent != null && parent != this) {
133                        child = parent.find(field);
134                        if (child != null) {
135                                child = ofChild(field, child.object());
136                        }
137                }
138                return child;
139        }
140
141        /**
142         * The object being wrapped.
143         * @return the Map, Iterable or object that was wrapped. Never <code>null</code>.
144         */
145        public Object object();
146
147        /**
148         * Convenience method for calling <code>toString</code> on the wrapped object.
149         * @return a toString on the wrapped object.
150         */
151        default String renderString() {
152                return String.valueOf(object());
153        }
154
155        /**
156         * The parent node.
157         * @return the parent node or <code>null</code> if this is the root.
158         */
159        default @Nullable ContextNode parent() {
160                return null;
161        }
162
163        /**
164         * If the node is a Map or a non iterable/array a singleton iterator will be returned.
165         * Otherwise if it is an interable/array new child context nodes will be created
166         * lazily.
167         * @return lazy iterator of context nodes.
168         */
169        @Override
170        default Iterator<ContextNode> iterator() {
171                Object o = object();
172                if (o instanceof Iterable<?> it) {
173                        AtomicInteger index = new AtomicInteger();
174                        return StreamSupport.stream(it.spliterator(), false).map(i -> this.ofChild(index.getAndIncrement(), i))
175                                        .iterator();
176                }
177                else if (o == null || Boolean.FALSE.equals(o)) {
178                        return Collections.emptyIterator();
179                }
180                else if (o.getClass().isArray()) {
181                        /*
182                         * There is probably an easier way to do this
183                         */
184                        Stream<? extends Object> s;
185                        if (o instanceof int[] a) {
186                                s = Arrays.stream(a).boxed();
187                        }
188                        else if (o instanceof long[] a) {
189                                s = Arrays.stream(a).boxed();
190                        }
191                        else if (o instanceof double[] a) {
192                                s = Arrays.stream(a).boxed();
193                        }
194                        else if (o instanceof boolean[] a) {
195                                List<Boolean> b = new ArrayList<>();
196                                for (var _a : a) {
197                                        b.add(_a);
198                                }
199                                s = b.stream();
200                        }
201                        else if (o instanceof char[] a) {
202                                List<Character> b = new ArrayList<>();
203                                for (var _a : a) {
204                                        b.add(_a);
205                                }
206                                s = b.stream();
207                        }
208                        else if (o instanceof byte[] a) {
209                                List<Byte> b = new ArrayList<>();
210                                for (var _a : a) {
211                                        b.add(_a);
212                                }
213                                s = b.stream();
214                        }
215                        else if (o instanceof float[] a) {
216                                List<Float> b = new ArrayList<>();
217                                for (var _a : a) {
218                                        b.add(_a);
219                                }
220                                s = b.stream();
221                        }
222                        else if (o instanceof short[] a) {
223                                List<Short> b = new ArrayList<>();
224                                for (var _a : a) {
225                                        b.add(_a);
226                                }
227                                s = b.stream();
228                        }
229                        else if (o instanceof Object[] a) {
230                                s = Arrays.asList(a).stream();
231                        }
232                        else {
233                                throw new IllegalArgumentException("array type not supported: " + o.getClass());
234                        }
235                        AtomicInteger index = new AtomicInteger();
236                        return s.map(i -> this.ofChild(index.getAndIncrement(), i)).iterator();
237                }
238
239                return Collections.singletonList(this).iterator();
240        }
241
242        /**
243         * Determines if an object is falsey based on mustache spec semantics where:
244         * <code>null</code>, empty iterables, empty arrays and boolean <code>false</code> are
245         * falsey.
246         * @param context a context object. ContextNode are allowed as input as well as
247         * <code>null</code>.
248         * @return true if the object is falsey.
249         */
250        static boolean isFalsey(@Nullable Object context) {
251                if (context == null) {
252                        return true;
253                }
254                if (Boolean.FALSE.equals(context)) {
255                        return true;
256                }
257                if (context instanceof Collection<?> c) {
258                        return c.isEmpty();
259                }
260                if (context instanceof Iterable<?> it) {
261                        return !it.iterator().hasNext();
262                }
263                if (context.getClass().isArray() && Array.getLength(context) == 0) {
264                        return false;
265                }
266                return false;
267        }
268
269}
270
271record RootContextNode(Object object) implements ContextNode {
272        @Override
273        public String toString() {
274                return renderString();
275        }
276}
277
278record NamedContextNode(ContextNode parent, Object object, String name) implements ContextNode {
279        @Override
280        public String toString() {
281                return renderString();
282        }
283}
284
285record IndexedContextNode(ContextNode parent, Object object, int index) implements ContextNode {
286        @Override
287        public String toString() {
288                return renderString();
289        }
290}