001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.bct.assertions;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022
023import java.io.*;
024import java.util.*;
025import java.util.function.*;
026
027import org.apache.juneau.commons.function.*;
028import org.apache.juneau.commons.utils.*;
029
030/**
031 * Used for fluent assertion calls against array objects.
032 *
033 * <h5 class='section'>Test Methods:</h5>
034 * <p>
035 * <ul class='javatree'>
036 *    <li class='jc'>{@link FluentArrayAssertion}
037 *    <ul class='javatreec'>
038 *       <li class='jm'>{@link FluentArrayAssertion#isHas(Object[]) isHas(Object[])}
039 *       <li class='jm'>{@link FluentArrayAssertion#is(Predicate) is(Predicate)}
040 *       <li class='jm'>{@link FluentArrayAssertion#isAny(Predicate) isAny(Predicate)}
041 *       <li class='jm'>{@link FluentArrayAssertion#isAll(Predicate) isAll(Predicate)}
042 *       <li class='jm'>{@link FluentArrayAssertion#isEmpty() isEmpty()}
043 *       <li class='jm'>{@link FluentArrayAssertion#isNotEmpty() isNotEmpty()}
044 *       <li class='jm'>{@link FluentArrayAssertion#isSize(int size) isSize(int size)}
045 *       <li class='jm'>{@link FluentArrayAssertion#isContains(Object) isContains(Object)}
046 *       <li class='jm'>{@link FluentArrayAssertion#isNotContains(Object) isNotContains(Object)}
047 *    </ul>
048 *    <li class='jc'>{@link FluentObjectAssertion}
049 *    <ul class='javatreec'>
050 *       <li class='jm'>{@link FluentObjectAssertion#isExists() isExists()}
051 *       <li class='jm'>{@link FluentObjectAssertion#is(Object) is(Object)}
052 *       <li class='jm'>{@link FluentObjectAssertion#is(Predicate) is(Predicate)}
053 *       <li class='jm'>{@link FluentObjectAssertion#isNot(Object) isNot(Object)}
054 *       <li class='jm'>{@link FluentObjectAssertion#isAny(Object...) isAny(Object...)}
055 *       <li class='jm'>{@link FluentObjectAssertion#isNotAny(Object...) isNotAny(Object...)}
056 *       <li class='jm'>{@link FluentObjectAssertion#isNull() isNull()}
057 *       <li class='jm'>{@link FluentObjectAssertion#isNotNull() isNotNull()}
058 *       <li class='jm'>{@link FluentObjectAssertion#isString(String) isString(String)}
059 *       <li class='jm'>{@link FluentObjectAssertion#isSame(Object) isSame(Object)}
060 *       <li class='jm'>{@link FluentObjectAssertion#isType(Class) isType(Class)}
061 *       <li class='jm'>{@link FluentObjectAssertion#isExactType(Class) isExactType(Class)}
062 *    </ul>
063 * </ul>
064 *
065 * <h5 class='section'>Transform Methods:</h5>
066 * <p>
067 * <ul class='javatree'>
068 *    <li class='jc'>{@link FluentArrayAssertion}
069 *    <ul class='javatreec'>
070 *       <li class='jm'>{@link FluentArrayAssertion#asStrings() asStrings()}
071 *       <li class='jm'>{@link FluentArrayAssertion#asStrings(Function) asStrings(Function)}
072 *       <li class='jm'>{@link FluentArrayAssertion#asCdl() asCdl()}
073 *       <li class='jm'>{@link FluentArrayAssertion#asCdl(Function) asCdl(Function)}
074 *       <li class='jm'>{@link FluentArrayAssertion#asBeanList() asBeanList()}
075 *       <li class='jm'>{@link FluentArrayAssertion#asItem(int) asItem(int)}
076 *       <li class='jm'>{@link FluentArrayAssertion#asSorted() asSorted()}
077 *       <li class='jm'>{@link FluentArrayAssertion#asSorted(Comparator) asSorted(Comparator)}
078 *    </ul>
079 *    <li class='jc'>{@link FluentObjectAssertion}
080 *    <ul class='javatreec'>
081 *       <li class='jm'>{@link FluentObjectAssertion#asString() asString()}
082 *       <li class='jm'>{@link FluentObjectAssertion#asString(ThrowingFunction) asString(ThrowingFunction)}
083 *       <li class='jm'>{@link FluentObjectAssertion#asTransformed(UnaryOperator) asTransformed(UnaryOperator)}
084 *       <li class='jm'>{@link FluentObjectAssertion#asAny() asAny()}
085 * </ul>
086 * </ul>
087 *
088 * <h5 class='section'>Configuration Methods:</h5>
089 * <p>
090 * <ul class='javatree'>
091 *    <li class='jc'>{@link Assertion}
092 *    <ul class='javatreec'>
093 *       <li class='jm'>{@link Assertion#setMsg(String, Object...) setMsg(String, Object...)}
094 *       <li class='jm'>{@link Assertion#setOut(PrintStream) setOut(PrintStream)}
095 *       <li class='jm'>{@link Assertion#setSilent() setSilent()}
096 *       <li class='jm'>{@link Assertion#setStdOut() setStdOut()}
097 *       <li class='jm'>{@link Assertion#setThrowable(Class) setThrowable(Class)}
098 *    </ul>
099 * </ul>
100 *
101 * <h5 class='section'>See Also:</h5><ul>
102 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauEcosystemOverview">Juneau Ecosystem</a>
103 * </ul>
104 *
105 * @param <E> The entry type.
106 * @param <R> The return type.
107 */
108@SuppressWarnings({
109   "java:S115", // Constants use UPPER_snakeCase convention
110   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
111})
112public class FluentArrayAssertion<E,R> extends FluentObjectAssertion<E[],R> {
113
114   private static final Messages MESSAGES = Messages.of(FluentArrayAssertion.class, "Messages");
115   private static final String MSG_arrayWasEmpty = MESSAGES.getString("arrayWasEmpty");
116   private static final String MSG_arrayWasNotEmpty = MESSAGES.getString("arrayWasNotEmpty");
117   private static final String MSG_arrayUnexpectedSize = MESSAGES.getString("arrayUnexpectedSize");
118   private static final String MSG_arrayDidNotContainExpectedValue = MESSAGES.getString("arrayDidNotContainExpectedValue");
119   private static final String MSG_arrayContainedUnexpectedValue = MESSAGES.getString("arrayContainedUnexpectedValue");
120   private static final String MSG_arrayDidNotContainExpectedValueAt = MESSAGES.getString("arrayDidNotContainExpectedValueAt");
121   private static final String MSG_arrayDidntContainAnyMatchingValue = MESSAGES.getString("arrayDidntContainAnyMatchingValue");
122   private static final String MSG_arrayContainedNonMatchingValueAt = MESSAGES.getString("arrayContainedNonMatchingValueAt");
123
124   /**
125    * Chained constructor.
126    *
127    * <p>
128    * Used when transforming one assertion into another so that the assertion config can be used by the new assertion.
129    *
130    * @param creator
131    *    The assertion that created this assertion.
132    *    <br>Should be <jk>null</jk> if this is the top-level assertion.
133    * @param value
134    *    The object being tested.
135    *    <br>Can be <jk>null</jk> (assertions such as {@code isNull()} then operate on the absence of a value).
136    * @param returns
137    *    The object to return after a test method is called.
138    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
139    * used on the same assertion.
140    */
141   public FluentArrayAssertion(Assertion creator, E[] value, R returns) {
142      super(creator, value, returns);
143   }
144
145   /**
146    * Constructor.
147    *
148    * @param value
149    *    The object being tested.
150    *    <br>Can be <jk>null</jk> (assertions such as {@code isNull()} then operate on the absence of a value).
151    * @param returns
152    *    The object to return after a test method is called.
153    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
154    * used on the same assertion.
155    */
156   public FluentArrayAssertion(E[] value, R returns) {
157      this(null, value, returns);
158   }
159
160   /**
161    * Converts this assertion into a {@link FluentBeanListAssertion}.
162    *
163    * <h5 class='section'>Example:</h5>
164    * <p class='bjava'>
165    *    <jc>// Extracts the 'foo' property from an array of beans and validates their values.</jc>.
166    *    <jsm>assertObject</jsm>(<jv>myArrayOfBeans</jv>)
167    *       .asBeanList()
168    *       .asProperty(<js>"foo"</js>)
169    *       .asSorted()
170    *       .equals(<js>"value1"</js>,<js>"value2"</js>,<js>"value3"</js>);
171    * </p>
172    *
173    * @return A new fluent string assertion containing a copy of the values in this assertion.
174    */
175   public FluentBeanListAssertion<E,R> asBeanList() {
176      return new FluentBeanListAssertion<>(this, toList(), returns());
177   }
178
179   /**
180    * Converts the entries in this list to a simple comma-delimited list and returns the value as a fluent string assertion.
181    *
182    * @return A fluent string assertion.  Never <jk>null</jk>.
183    */
184   public FluentStringAssertion<R> asCdl() {
185      return new FluentStringAssertion<>(this, valueIsNull() ? null : join(value(), ','), returns());
186   }
187
188   /**
189    * Converts the entries to strings using the specified stringify function, combines them into a simple comma-delimited list, and returns the value as a fluent string assertion.
190    *
191    * @param function The function to apply to all values in this list.
192    * @return A fluent string assertion.  Never <jk>null</jk>.
193    */
194   public FluentStringAssertion<R> asCdl(Function<E,String> function) {
195      List<String> l = valueIsNull() ? null : stream(value()).map(function::apply).toList();
196      return new FluentStringAssertion<>(this, join(l, ','), returns());
197   }
198
199   /**
200    * Returns an object assertion on the item specified at the specified index.
201    *
202    * <p>
203    * If the array is <jk>null</jk> or the index is out-of-bounds, the returned assertion is a null assertion
204    * (meaning {@link FluentAnyAssertion#isExists()} returns <jk>false</jk>).
205    *
206    * @param index The index of the item to retrieve from the array.
207    * @return A new assertion.
208    */
209   public FluentAnyAssertion<E,R> asItem(int index) {
210      return new FluentAnyAssertion<>(this, at(index), returns());
211   }
212
213   /**
214    * Sorts the entries in this list.
215    *
216    * @return A new list assertion.  The contents of the original list remain unchanged.
217    */
218   public FluentListAssertion<E,R> asSorted() {
219      return new FluentListAssertion<>(this, toSortedList(null), returns());
220   }
221
222   /**
223    * Sorts the entries in this list using the specified comparator.
224    *
225    * @param comparator The comparator to use to sort the list.
226    * @return A new list assertion.  The contents of the original list remain unchanged.
227    */
228   public FluentListAssertion<E,R> asSorted(Comparator<E> comparator) {
229      return new FluentListAssertion<>(this, toSortedList(comparator), returns());
230   }
231
232   @Override /* Overridden from FluentBaseAssertion */
233   public FluentStringAssertion<R> asString() {
234      return new FluentStringAssertion<>(this, valueIsNull() ? null : r(value()), returns());
235   }
236
237   /**
238    * Converts this assertion into a {@link FluentListAssertion} of strings.
239    *
240    * @return A new fluent string assertion.
241    */
242   public FluentStringListAssertion<R> asStrings() {
243      return new FluentStringListAssertion<>(this, valueIsNull() ? null : stream(value()).map(Shorts::s).toList(), returns());
244   }
245
246   /**
247    * Runs the stringify function against all values in this list and returns it as a fluent string list assertion.
248    *
249    * @param function The function to apply to all values in this list.
250    * @return A new fluent string list assertion.  Never <jk>null</jk>.
251    */
252   public FluentStringListAssertion<R> asStrings(Function<E,String> function) {
253      List<String> l = valueIsNull() ? null : stream(value()).map(function::apply).toList();
254      return new FluentStringListAssertion<>(this, l, returns());
255   }
256
257   @Override /* Overridden from FluentObjectAssertion */
258   public FluentArrayAssertion<E,R> asTransformed(UnaryOperator<E[]> function) {
259      return new FluentArrayAssertion<>(this, function.apply(orElse(null)), returns());
260   }
261
262   /**
263    * Asserts that the contents of this list pass the specified tests.
264    *
265    * @param tests The tests to run.  <jk>null</jk> entries are ignored.
266    * @return This object.
267    * @throws AssertionError If assertion failed.
268    */
269   @SafeVarargs
270   public final R is(Predicate<E>...tests) throws AssertionError {
271      isSize(tests.length);
272      for (int i = 0, j = length(); i < j; i++) {
273         var t = tests[i];
274         if (nn(t) && ! t.test(at(i)))
275            throw error(MSG_arrayDidNotContainExpectedValueAt, i, getFailureMessage(t, at(i)));
276      }
277      return returns();
278   }
279
280   /**
281    * Asserts that all values in the array passes the specified test.
282    *
283    * @param test The predicate test.
284    * @return The fluent return object.
285    * @throws AssertionError If assertion failed or value was <jk>null</jk>.
286    */
287   public R isAll(Predicate<E> test) throws AssertionError {
288      reqnn("test", test);
289      for (int i = 0, j = length(); i < j; i++)
290         if (! test.test(at(i)))
291            throw error(MSG_arrayContainedNonMatchingValueAt, i, getFailureMessage(test, at(i)));
292      return returns();
293   }
294
295   /**
296    * Asserts that at least one value in the array passes the specified test.
297    *
298    * @param test The predicate test.
299    * @return The fluent return object.
300    * @throws AssertionError If assertion failed or value was <jk>null</jk>.
301    */
302   public R isAny(Predicate<E> test) throws AssertionError {
303      reqnn("test", test);
304      for (var v : value())
305         if (test.test(v))
306            return returns();
307      throw error(MSG_arrayDidntContainAnyMatchingValue, r(value()));
308   }
309
310   /**
311    * Asserts that the array contains the expected value.
312    *
313    * @param entry The value to check for.
314    * @return The fluent return object.
315    * @throws AssertionError If assertion failed.
316    */
317   public R isContains(E entry) throws AssertionError {
318      for (int i = 0, j = length(); i < j; i++)
319         if (eq(at(i), entry))
320            return returns();
321      throw error(MSG_arrayDidNotContainExpectedValue, r(entry), r(value()));
322   }
323
324   /**
325    * Asserts that the collection exists and is empty.
326    *
327    * @return The fluent return object.
328    * @throws AssertionError If assertion failed.
329    */
330   public R isEmpty() throws AssertionError {
331      if (length() != 0)
332         throw error(MSG_arrayWasNotEmpty);
333      return returns();
334   }
335
336   /**
337    * Asserts that the contents of this list contain the specified values.
338    *
339    * @param entries The expected entries in this list.
340    * @return This object.
341    * @throws AssertionError If assertion failed.
342    */
343   @SuppressWarnings({
344      "unchecked" // Type erasure requires cast for array has check
345   })
346   public R isHas(E...entries) throws AssertionError {
347      reqnn("entries", entries);
348      Predicate<E>[] p = stream(entries).map(AssertionPredicates::eq).toArray(Predicate[]::new);
349      return is(p);
350   }
351
352   /**
353    * Asserts that the array does not contain the expected value.
354    *
355    * @param entry The value to check for.
356    * @return The fluent return object.
357    * @throws AssertionError If assertion failed.
358    */
359   public R isNotContains(E entry) throws AssertionError {
360      for (int i = 0, j = length(); i < j; i++)
361         if (eq(at(i), entry))
362            throw error(MSG_arrayContainedUnexpectedValue, r(entry), r(value()));
363      return returns();
364   }
365
366   /**
367    * Asserts that the collection exists and is not empty.
368    *
369    * @return The fluent return object.
370    * @throws AssertionError If assertion failed.
371    */
372   public R isNotEmpty() throws AssertionError {
373      if (length() == 0)
374         throw error(MSG_arrayWasEmpty);
375      return returns();
376   }
377
378   /**
379    * Asserts that the collection exists and is the specified size.
380    *
381    * @param size The expected size.
382    * @return The fluent return object.
383    * @throws AssertionError If assertion failed.
384    */
385   public R isSize(int size) throws AssertionError {
386      if (length() != size)
387         throw error(MSG_arrayUnexpectedSize, size, length());
388      return returns();
389   }
390
391   @Override /* Overridden from Assertion */
392   public FluentArrayAssertion<E,R> setMsg(String msg, Object...args) {
393      super.setMsg(msg, args);
394      return this;
395   }
396
397   @Override /* Overridden from Assertion */
398   public FluentArrayAssertion<E,R> setOut(PrintStream value) {
399      super.setOut(value);
400      return this;
401   }
402
403   @Override /* Overridden from Assertion */
404   public FluentArrayAssertion<E,R> setSilent() {
405      super.setSilent();
406      return this;
407   }
408
409   @Override /* Overridden from Assertion */
410   public FluentArrayAssertion<E,R> setStdOut() {
411      super.setStdOut();
412      return this;
413   }
414
415   @Override /* Overridden from Assertion */
416   public FluentArrayAssertion<E,R> setThrowable(Class<? extends RuntimeException> value) {
417      super.setThrowable(value);
418      return this;
419   }
420
421   @Override
422   public String toString() {
423      return valueIsNull() ? null : Arrays.toString(value());
424   }
425
426   private E at(int index) {
427      return valueIsNull() || index >= length() || index < 0 ? null : value()[index];
428   }
429
430   private int length() {
431      return value().length;
432   }
433
434   private List<E> toList() {
435      return valueIsNull() ? null : list(value());
436   }
437
438   private List<E> toSortedList(Comparator<E> comparator) {
439      return valueIsNull() ? null : sortedList(comparator, value());
440   }
441}