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 java.util.Arrays.*;
020import static java.util.Collections.*;
021import static java.util.stream.Collectors.*;
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 maps.
032 *
033 * <h5 class='section'>Test Methods:</h5>
034 * <p>
035 * <ul class='javatree'>
036 *    <li class='jc'>{@link FluentMapAssertion}
037 *    <ul class='javatreec'>
038 *       <li class='jm'>{@link FluentMapAssertion#isEmpty() isEmpty()}
039 *       <li class='jm'>{@link FluentMapAssertion#isNotEmpty() isNotEmpty()}
040 *       <li class='jm'>{@link FluentMapAssertion#isContainsKey(String) isContainsKey(String)}
041 *       <li class='jm'>{@link FluentMapAssertion#isNotContainsKey(String) isNotContainsKey(String)}
042 *       <li class='jm'>{@link FluentMapAssertion#isSize(int) isSize(int)}
043 *    </ul>
044 *    <li class='jc'>{@link FluentObjectAssertion}
045 *    <ul class='javatreec'>
046 *       <li class='jm'>{@link FluentObjectAssertion#isExists() isExists()}
047 *       <li class='jm'>{@link FluentObjectAssertion#is(Object) is(Object)}
048 *       <li class='jm'>{@link FluentObjectAssertion#is(Predicate) is(Predicate)}
049 *       <li class='jm'>{@link FluentObjectAssertion#isNot(Object) isNot(Object)}
050 *       <li class='jm'>{@link FluentObjectAssertion#isAny(Object...) isAny(Object...)}
051 *       <li class='jm'>{@link FluentObjectAssertion#isNotAny(Object...) isNotAny(Object...)}
052 *       <li class='jm'>{@link FluentObjectAssertion#isNull() isNull()}
053 *       <li class='jm'>{@link FluentObjectAssertion#isNotNull() isNotNull()}
054 *       <li class='jm'>{@link FluentObjectAssertion#isString(String) isString(String)}
055 *       <li class='jm'>{@link FluentObjectAssertion#isSame(Object) isSame(Object)}
056 *       <li class='jm'>{@link FluentObjectAssertion#isType(Class) isType(Class)}
057 *       <li class='jm'>{@link FluentObjectAssertion#isExactType(Class) isExactType(Class)}
058 *    </ul>
059 * </ul>
060 *
061 * <h5 class='section'>Transform Methods:</h5>
062 * <p>
063 * <ul class='javatree'>
064 *    <li class='jc'>{@link FluentMapAssertion}
065 *    <ul class='javatreec'>
066 *       <li class='jm'>{@link FluentMapAssertion#asValue(Object) asValue(Object)}
067 *       <li class='jm'>{@link FluentMapAssertion#asValues(Object...) asValues(Object...)}
068 *       <li class='jm'>{@link FluentMapAssertion#asValueMap(Object...) asValueMap(Object...)}
069 *       <li class='jm'>{@link FluentMapAssertion#asSize() asSize()}
070 *    </ul>
071 *    <li class='jc'>{@link FluentObjectAssertion}
072 *    <ul class='javatreec'>
073 *       <li class='jm'>{@link FluentObjectAssertion#asString() asString()}
074 *       <li class='jm'>{@link FluentObjectAssertion#asString(ThrowingFunction) asString(ThrowingFunction)}
075 *       <li class='jm'>{@link FluentObjectAssertion#asTransformed(UnaryOperator) asTransformed(UnaryOperator)}
076 *       <li class='jm'>{@link FluentObjectAssertion#asAny() asAny()}
077 * </ul>
078 * </ul>
079 *
080 * <h5 class='section'>Configuration Methods:</h5>
081 * <p>
082 * <ul class='javatree'>
083 *    <li class='jc'>{@link Assertion}
084 *    <ul class='javatreec'>
085 *       <li class='jm'>{@link Assertion#setMsg(String, Object...) setMsg(String, Object...)}
086 *       <li class='jm'>{@link Assertion#setOut(PrintStream) setOut(PrintStream)}
087 *       <li class='jm'>{@link Assertion#setSilent() setSilent()}
088 *       <li class='jm'>{@link Assertion#setStdOut() setStdOut()}
089 *       <li class='jm'>{@link Assertion#setThrowable(Class) setThrowable(Class)}
090 *    </ul>
091 * </ul>
092 *
093 * <h5 class='section'>See Also:</h5><ul>
094 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauEcosystemOverview">Juneau Ecosystem</a>
095 * </ul>
096 *
097 * @param <K> The key type.
098 * @param <V> The value type.
099 * @param <R> The return type.
100 */
101@SuppressWarnings({
102   "java:S115", // Constants use UPPER_snakeCase convention (e.g., MSG_mapWasNotEmpty)
103   "unchecked" // Generic varargs parameters (K...keys) in the key-based assertion methods create generic arrays at call sites
104})
105public class FluentMapAssertion<K,V,R> extends FluentObjectAssertion<Map<K,V>,R> {
106
107   private static final Messages MESSAGES = Messages.of(FluentMapAssertion.class, "Messages");
108   private static final String MSG_mapWasNotEmpty = MESSAGES.getString("mapWasNotEmpty");
109   private static final String MSG_mapDidNotContainExpectedKey = MESSAGES.getString("mapDidNotContainExpectedKey");
110   private static final String MSG_mapContainedUnexpectedKey = MESSAGES.getString("mapContainedUnexpectedKey");
111   private static final String MSG_mapWasEmpty = MESSAGES.getString("mapWasEmpty");
112   private static final String MSG_mapDidNotHaveTheExpectedSize = MESSAGES.getString("mapDidNotHaveTheExpectedSize");
113
114   /**
115    * Chained constructor.
116    *
117    * <p>
118    * Used when transforming one assertion into another so that the assertion config can be used by the new assertion.
119    *
120    * @param creator
121    *    The assertion that created this assertion.
122    *    <br>Should be <jk>null</jk> if this is the top-level assertion.
123    * @param value
124    *    The object being tested.
125    *    <br>Can be <jk>null</jk> (assertions such as {@code isNull()} then operate on the absence of a value).
126    * @param returns
127    *    The object to return after a test method is called.
128    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
129    * used on the same assertion.
130    */
131   public FluentMapAssertion(Assertion creator, Map<K,V> value, R returns) {
132      super(creator, value, returns);
133   }
134
135   /**
136    * Constructor.
137    *
138    * @param value
139    *    The object being tested.
140    *    <br>Can be <jk>null</jk> (assertions such as {@code isNull()} then operate on the absence of a value).
141    * @param returns
142    *    The object to return after a test method is called.
143    *    <br>If <jk>null</jk>, the test method returns this object allowing multiple test method calls to be
144    * used on the same assertion.
145    */
146   public FluentMapAssertion(Map<K,V> value, R returns) {
147      this(null, value, returns);
148   }
149
150   /**
151    * Returns an integer assertion on the size of this map.
152    *
153    * <p>
154    * If the map is <jk>null</jk>, the returned assertion is a null assertion
155    * (meaning {@link FluentIntegerAssertion#isExists()} returns <jk>false</jk>).
156    *
157    * @return A new assertion.
158    */
159   public FluentIntegerAssertion<R> asSize() {
160      return new FluentIntegerAssertion<>(this, valueIsNull() ? null : value().size(), returns());
161   }
162
163   @Override /* Overridden from FluentObjectAssertion */
164   public FluentMapAssertion<K,V,R> asTransformed(UnaryOperator<Map<K,V>> function) {
165      return new FluentMapAssertion<>(this, function.apply(orElse(null)), returns());
166   }
167
168   /**
169    * Returns an object assertion on the value specified at the specified key.
170    *
171    * <p>
172    * If the map is <jk>null</jk> or the map doesn't contain the specified key, the returned assertion is a null assertion
173    * (meaning {@link FluentAnyAssertion#isExists()} returns <jk>false</jk>).
174    *
175    * @param key The key of the item to retrieve from the map.
176    * @return A new assertion.
177    */
178   public FluentAnyAssertion<V,R> asValue(K key) {
179      return new FluentAnyAssertion<>(this, get(key), returns());
180   }
181
182   /**
183    * Extracts a subset of this map.
184    *
185    * @param keys The entries to extract.
186    * @return This object.
187    */
188   public FluentMapAssertion<K,V,R> asValueMap(K...keys) {
189      if (valueIsNull())
190         return new FluentMapAssertion<>(this, null, returns());
191      var m1 = value();
192      Map<K,V> m2 = CollectionUtils.map();
193      for (var k : keys)
194         m2.put(k, m1.get(k));
195      return new FluentMapAssertion<>(this, m2, returns());
196   }
197
198   /**
199    * Returns a {@link FluentListAssertion} of the values of the specified keys.
200    *
201    * If the map is <jk>null</jk>, the returned assertion is a null assertion
202    * (meaning {@link FluentObjectAssertion#isExists()} returns <jk>false</jk>).
203    *
204    * @param keys The keys of the values to retrieve from the map.
205    * @return A new assertion.
206    */
207   public FluentListAssertion<Object,R> asValues(K...keys) {
208      return new FluentListAssertion<>(this, valueIsNull() ? null : stream(keys).map(this::get).collect(toList()), returns());
209   }
210
211   /**
212    * Asserts that the map contains the expected key.
213    *
214    * @param name The key name to check for.
215    * @return The fluent return object.
216    * @throws AssertionError If assertion failed or value was <jk>null</jk>.
217    */
218   public R isContainsKey(String name) throws AssertionError {
219      if (value().containsKey(name))
220         return returns();
221      throw error(MSG_mapDidNotContainExpectedKey, name, value());
222   }
223
224   /**
225    * Asserts that the map exists and is empty.
226    *
227    * @return The fluent return object.
228    * @throws AssertionError If assertion failed or value was <jk>null</jk>.
229    */
230   public R isEmpty() throws AssertionError {
231      if (! value().isEmpty())
232         throw error(MSG_mapWasNotEmpty);
233      return returns();
234   }
235
236   /**
237    * Asserts that the map contains the expected key.
238    *
239    * @param name The key name to check for.
240    * @return The fluent return object.
241    * @throws AssertionError If assertion failed or value was <jk>null</jk>.
242    */
243   public R isNotContainsKey(String name) throws AssertionError {
244      if (! value().containsKey(name))
245         return returns();
246      throw error(MSG_mapContainedUnexpectedKey, name, value());
247   }
248
249   /**
250    * Asserts that the map exists and is not empty.
251    *
252    * @return The fluent return object.
253    * @throws AssertionError If assertion failed or value was <jk>null</jk>.
254    */
255   public R isNotEmpty() throws AssertionError {
256      if (value().isEmpty())
257         throw error(MSG_mapWasEmpty);
258      return returns();
259   }
260
261   /**
262    * Asserts that the map exists and is the specified size.
263    *
264    * @param size The expected size.
265    * @return The fluent return object.
266    * @throws AssertionError If assertion failed or value was <jk>null</jk>.
267    */
268   public R isSize(int size) throws AssertionError {
269      if (size2() != size)
270         throw error(MSG_mapDidNotHaveTheExpectedSize, size, size2());
271      return returns();
272   }
273
274   @Override /* Overridden from Assertion */
275   public FluentMapAssertion<K,V,R> setMsg(String msg, Object...args) {
276      super.setMsg(msg, args);
277      return this;
278   }
279
280   @Override /* Overridden from Assertion */
281   public FluentMapAssertion<K,V,R> setOut(PrintStream value) {
282      super.setOut(value);
283      return this;
284   }
285
286   @Override /* Overridden from Assertion */
287   public FluentMapAssertion<K,V,R> setSilent() {
288      super.setSilent();
289      return this;
290   }
291
292   @Override /* Overridden from Assertion */
293   public FluentMapAssertion<K,V,R> setStdOut() {
294      super.setStdOut();
295      return this;
296   }
297
298   @Override /* Overridden from Assertion */
299   public FluentMapAssertion<K,V,R> setThrowable(Class<? extends RuntimeException> value) {
300      super.setThrowable(value);
301      return this;
302   }
303
304   private V get(K key) {
305      return orElse(emptyMap()).get(key);
306   }
307
308   private int size2() {
309      return value().size();
310   }
311}