From de95bab70dfbf605fa369358444f11990d872bc0 Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Tue, 7 Dec 2021 15:37:55 +0100 Subject: [PATCH] [docs] Update with the latest changes and use compilable examples (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: István Zoltán Szabó --- docs/api-conventions.asciidoc | 249 ++++++++++-------- docs/connecting.asciidoc | 48 +--- docs/index-local.asciidoc | 4 + docs/index.asciidoc | 11 +- docs/installation.asciidoc | 22 +- docs/introduction.asciidoc | 43 +-- docs/local/README.md | 1 + .../docs/java-rest/low-level/index.asciidoc | 4 + docs/migrate.asciidoc | 18 +- .../documentation/ApiConventionsTest.java | 21 ++ 10 files changed, 221 insertions(+), 200 deletions(-) create mode 100644 docs/index-local.asciidoc create mode 100644 docs/local/README.md create mode 100644 docs/local/elasticsearch/docs/java-rest/low-level/index.asciidoc diff --git a/docs/api-conventions.asciidoc b/docs/api-conventions.asciidoc index 5ad0c3ec0..9fc71b53a 100644 --- a/docs/api-conventions.asciidoc +++ b/docs/api-conventions.asciidoc @@ -1,7 +1,7 @@ [[api-conventions]] == API conventions -The Java client uses a very consistent code structure, using modern code +The {java-client} uses a very consistent code structure, using modern code patterns that make complex requests easier to write and complex responses easier to process. This page explains these so that you quickly feel at home. @@ -11,19 +11,22 @@ to process. This page explains these so that you quickly feel at home. The {es} API is large and is organized into feature groups, as can be seen in the {ref}/rest-apis.html[{es} API documentation]. -The Java client follows this structure: feature groups are called “namespaces”, +The {java-client} follows this structure: feature groups are called “namespaces”, and each namespace is located in a subpackage of -`co.elastic.clients.elasticsearch`. The only exceptions are the “search” and -“document” APIs which are located in the `_core` subpackage. +`co.elastic.clients.elasticsearch`. + +Each of the namespace clients can be accessed from the top level {es} client. The +only exceptions are the “search” and “document” APIs which are located in the `core` +subpackage and can be accessed on the main {es} client object. -Each of the namespace clients can be accessed from the top level {es} client. The snippet below shows how to use the indices namespace client to create an -index: +index (the lambda syntax is <>): ["source","java"] -------------------------------------------------- -ElasticsearchClient esClient = ... -esClient.indices().create(c -> c.index("my-index")); +// Create the "products" index +ElasticsearchClient client = ... +client.indices().create(c -> c.index("products")); -------------------------------------------------- Namespace clients are very lightweight objects that can be created on the fly. @@ -32,108 +35,122 @@ Namespace clients are very lightweight objects that can be created on the fly. [discrete] === Method naming conventions -Classes in the Java API Client contain two kinds of methods and properties: +Classes in the {java-client} contain two kinds of methods and properties: * Methods and properties that are part of the API, such as `ElasticsearchClient.search()` or `SearchResponse.maxScore()`. They are derived from their respective names in the {es} JSON API using the standard Java -`camelCaseName` convention. +`camelCaseNaming` convention. * Methods and properties that are part of the framework on which the Java API -Client is built, such as `Query._type()`. These methods and properties are -prefixed with an underscore to both avoid any naming conflicts with API names -and ease distinguishing what identifiers belong to the API or to the framework. +Client is built, such as `Query._kind()`. These methods and properties are +prefixed with an underscore to both avoid any naming conflicts with API names, +and as an easy way to distinguish the API from the framework. [discrete] +[[builder-lambdas]] === Immutable objects, builders and builder lambdas -All data types in the Java client are immutable. Object creation uses the +All data types in the {java-client} are immutable. Object creation uses the https://www.informit.com/articles/article.aspx?p=1216151&seqNum=2[builder pattern] that was popularized in *Effective Java* in 2008. ["source","java"] -------------------------------------------------- -CreateResponse createResponse = client.indices().create( - new CreateRequest.Builder() - .index("my-index") - .putAliases("foo", - new Alias.Builder().isWriteIndex(true).build() - ) - .build() -); +ElasticsearchClient client = ... +include-tagged::{doc-tests}/ApiConventionsTest.java[builders] -------------------------------------------------- Note that a builder should not be reused after its `build()` method has been called. Although this works nicely, having to instantiate builder classes and call the -build() method is a bit verbose. So every builder setter in the Java client also +`build()` method is a bit verbose. So every property setter in the {java-client} also accepts a lambda expression that takes a newly created builder as a parameter -and returns a populated builder. The snippet above can be written also as: +and returns a populated builder. The snippet above can also be written as: ["source","java"] -------------------------------------------------- -CreateResponse createResponse = client.indices() - .create(createBuilder -> createBuilder - .index("my-index") - .putAliases("foo", aliasBuilder -> aliasBuilder - .isWriteIndex(true) - ) - ); +ElasticsearchClient client = ... +include-tagged::{doc-tests}/ApiConventionsTest.java[builder-lambdas] -------------------------------------------------- This approach allows for much more concise code, and also avoids importing classes (and even remembering their names) since types are inferred from the method parameter signature. -It becomes particularly useful with complex nested queries like the one below, -taken from the +Note in the above example that builder variables are only used to start a chain +of property setters. The names of these variables are therefore unimportant and +can be shortened to improve readability: + +["source","java"] +-------------------------------------------------- +ElasticsearchClient client = ... +include-tagged::{doc-tests}/ApiConventionsTest.java[builder-lambdas-short] +-------------------------------------------------- + + +Builder lambdas become particularly useful with complex nested queries like the +one below, taken from the {ref}/query-dsl-intervals-query.html[intervals query API documentation]. -This example also shows a useful naming convention for builder parameters in -deeply nested structures: since we have to give them a name to comply with the -Java syntax (Kotlin would accept `it` and Scala a simple `_`), we name them with -an underscore followed by the depth of the item, i.e. `_0`, `_1`, and so on. -This removes the need for finding names and makes reading the code easier to -read by reducing the number of identifiers. +This example also highlights a useful naming convention for builder parameters in +deeply nested structures. For lambda expressions with a single argument, Kotlin +provides the implicit `it` parameter and Scala allows use of `_`. This can be approximated +in Java by using an underscore prefix followed by a number representing the depth +level (i.e. `_0`, `_1`, and so on). Not only does this remove the need to create +throw-away variable names, but it also improves code readability. Correct indentation +also allows the structure of the query to stand out. + +["source","java"] +-------------------------------------------------- +ElasticsearchClient client = ... +include-tagged::{doc-tests}/ApiConventionsTest.java[builder-intervals] +-------------------------------------------------- +<1> Search results will be mapped to `SomeApplicationData` instances to + be readily available to the application. + +[discrete] +=== Lists and maps + +[discrete] +==== Additive builder setters + +Properties of type `List` and `Map` are exposed by object builders as a set of overloaded +additive-only methods that _update_ the property value, by appending to lists and adding +new entries to maps (or replacing existing ones). + +Object builders create immutable objects, and this also applies to list and map properties +that are made immutable at object construction time. ["source","java"] -------------------------------------------------- -client.search(_0 -> _0 - .query(_1 -> _1 - .intervals(_2 -> _2 - .field("my_text") - .allOf(_3 -> _3 - .ordered(true) - .addIntervals(_4 -> _4 - .match(_5 -> _5 - .query("my favorite food") - .maxGaps(0) - .ordered(true) - ) - ) - .addIntervals(_4 -> _4 - .anyOf(_5 -> _5 - .addIntervals(_6 -> _6 - .match(_7 -> _7 - .query("hot water") - ) - ) - .addIntervals(_6 -> _6 - .match(_7 -> _7 - .query("cold porridge") - ) - ) - ) - ) - ) - ) - ), - RequestTest.AppData.class -); +include-tagged::{doc-tests}/ApiConventionsTest.java[collections] -------------------------------------------------- +[discrete] +==== List and map values are never `null` + +The {es} API has a lot of optional properties. For single-valued properties, the {java-client} +represents missing optional values as `null`. Applications therefore have to null-check +optional values before using them. + +For lists and maps however, applications often only care about whether they're empty or not, +or even just iterate on their content. Using `null` values is then cumbersome. To avoid this, +{java-client} collection properties are never `null`, and missing optional collections are +returned as an empty collection. + +If you ever need to distinguish between a missing (undefined) optional collection and an +effectively-empty collection returned by {es}, the `ApiTypeHelper` class provides a utility +method to distinguish them: + +["source","java"] +-------------------------------------------------- +include-tagged::{doc-tests}/ApiConventionsTest.java[optional-collections] +-------------------------------------------------- + + [discrete] === Variant types @@ -141,42 +158,57 @@ The {es} API has a lot of variant types: queries, aggregations, field mappings, analyzers, and so on. Finding the correct class name in such large collections can be challenging. -The Java client builders make this easy: the builders for variant types, such as -Query, have methods for each of the available implementations. We’ve seen this +The {java-client} builders make this easy: the builders for variant types, such as +`Query`, have methods for each of the available implementations. We’ve seen this in action above with `intervals` (a kind of query) and `allOf`, `match` and `anyOf` (various kinds of intervals). -This is because variant objects in the Java client are implementations of a -“tagged union”: they contain the identifier (or tag) of the variant they hold +This is because variant objects in the {java-client} are implementations of a +“tagged union”: they contain the identifier (or tag) of the variant they hold and the value for that variant. For example, a `Query` object can contain an `IntervalsQuery` with tag `intervals`, a `TermQuery` with tag `term`, and so on. This approach allows writing fluent code where you can let the IDE completion features guide you to build and navigate complex nested structures: -* Variant builders have setter methods for every available implementation. They - use the same conventions as regular properties and accept both a builder lambda - expression and a ready-made object of the actual type of the variant. Here’s an - example to build a term query: -+ --- +Variant builders have setter methods for every available implementation. They +use the same conventions as regular properties and accept both a builder lambda +expression and a ready-made object of the actual type of the variant. Here’s an +example to build a term query: + ["source","java"] -------------------------------------------------- -Query query = new Query.Builder() - .term( // <1> - t -> t.field("name").value("foo") // <2> - ) - .build(); // <3> - +include-tagged::{doc-tests}/ApiConventionsTest.java[variant-creation] -------------------------------------------------- <1> Choose the `term` variant to build a term query. <2> Build the terms query with a builder lambda expression. -<3> Build the `Query` that now holds a `TermQuery` object with tag `term`. --- +<3> Build the `Query` that now holds a `TermQuery` object of kind `term`. + +Variant objects have getter methods for every available implementation. These +methods check that the object actually holds a variant of that kind and return +the value downcasted to the correct type. They throw an `IllegalStateException` +otherwise. This approach allows writing fluent code to traverse variants. -* Variant objects have getter methods for every available implementation. These - methods check that the object actually holds a variant of that type and return - the value downcasted to the correct type. They throw an `IllegalStateException` - otherwise. This approach allows writing fluent code to traverse variants. +["source","java"] +-------------------------------------------------- +include-tagged::{doc-tests}/ApiConventionsTest.java[variant-navigation] +-------------------------------------------------- + +Variant objects also provide information on the variant kind they currently hold: + +* with `is` methods for each of the variant kinds: `isTerm()`, `isIntervals()`, `isFuzzy()`, etc. + +* with a nested `Kind` enumeration that defines all variant kinds. + +This information can then be used to navigate down into specific variants after checking +their actual kind: + +["source","java"] +-------------------------------------------------- +include-tagged::{doc-tests}/ApiConventionsTest.java[variant-kind] +-------------------------------------------------- +<1> Test if the variant is of a specific kind. +<2> Test a larger set of variant kinds. +<3> Get the kind and value held by the variant object. [discrete] === Blocking and asynchronous clients @@ -189,19 +221,9 @@ same transport object: ["source","java"] -------------------------------------------------- -Transport transport = ... +ElasticsearchTransport transport = ... -ElasticsearchClient client = new ElasticsearchClient(transport); -if (client.exists(b -> b.index("products").id("foo")).value()) { - logger.info("product exists"); -} - -ElasticsearchAsyncClient asyncClient = new ElasticsearchAsyncClient(transport); -asyncClient.exists(b -> b.index("products").id("foo")).thenAccept(response -> { - if (response.value()) { - logger.info("product exists"); - } -}); +include-tagged::{doc-tests}/ApiConventionsTest.java[blocking-and-async] -------------------------------------------------- [discrete] @@ -211,22 +233,21 @@ Client methods can throw two kinds of exceptions: * Requests that were received by the {es} server but that were rejected (validation error, server internal timeout exceeded, etc) will produce an -`ApiException`. This exception contains details about the error provided by -{es}. - -* Requests that fail to reach the server (network error, server unavailable, -etc) will produce a subclass `IOException`. That subclass is specific to the -transport used. In the case of the `RestClientTransport` it will be a -`ResponseException` that contains the low level HTTP response. +`ElasticsearchException`. This exception contains details about the error, +provided by {es}. +* Requests that failed to reach the server (network error, server unavailable, +etc) will produce a `TransportException`. That exception's cause is the exception +thrown by the lower-level implementation. In the case of the `RestClientTransport` +it will be a `ResponseException` that contains the low level HTTP response. [discrete] === Object life cycles -There are five kinds of objects in the Java client with different life cycles: +There are five kinds of objects in the {java-client} with different life cycles: -**Object mapper**:: +**Object mapper**:: Stateless and thread-safe, but can be costly to create. It’s usually a singleton that is created at application startup and used to create the transport. diff --git a/docs/connecting.asciidoc b/docs/connecting.asciidoc index 60b7a3920..a091d69a5 100644 --- a/docs/connecting.asciidoc +++ b/docs/connecting.asciidoc @@ -1,61 +1,39 @@ [[connecting]] == Connecting -beta[] +The {java-client} is structured around three main components: -The Java client is structured around three main components: - -* **API client classes**. These provide strongly typed data structures and -methods for {es} APIs. Since the {es} API is large, it is structured in feature -groups (also called “namespaces”), each having its own client class. {es} core +* **API client classes**. These provide strongly typed data structures and +methods for {es} APIs. Since the {es} API is large, it is structured in feature +groups (also called “namespaces”), each having its own client class. {es} core features are implemented in the `ElasticsearchClient` class. -* **A JSON object mapper**. This maps your application classes to JSON and +* **A JSON object mapper**. This maps your application classes to JSON and seamlessly integrates them with the API client. -* **A transport layer implementation**. This is where all HTTP request handling +* **A transport layer implementation**. This is where all HTTP request handling takes place. This code snippet creates and wires together these three components: ["source","java"] -------------------------------------------------- -// Create the low-level client -RestClient restClient = RestClient.builder( - new HttpHost("localhost", 9200)).build(); - -// Create the transport with a Jackson mapper -Transport transport = new RestClientTransport( - restClient, new JacksonJsonpMapper()); - -// And create the API client -ElasticsearchClient client = new ElasticsearchClient(transport); +include-tagged::{doc-tests}/ConnectingTest.java[create-client] -------------------------------------------------- -Authentication is managed by the <>. For further details on -configuring authentication, refer to +Authentication is managed by the <>. For further details on +configuring authentication, refer to {java-api-client}/_basic_authentication.html[its documentation]. [discrete] === Your first request -The code snippet below searches all items from a “product” index whose name +The code snippet below searches all items from a “product” index whose name matches “bicycle” and return them as instances of a `Product` application class. -It illustrates the use of fluent functional builders to write search queries as -concise DSL-like code. This pattern is explained in more detail in +It illustrates the use of fluent functional builders to write search queries as +concise DSL-like code. This pattern is explained in more detail in <>. ["source","java"] -------------------------------------------------- -SearchResponse search = client.search(s -> s - .index("products") - .query(q -> q - .term(t -> t - .field("name") - .value("bicycle") - )), - Product.class); - -for (Hit hit: search.hits().hits()) { - processAppData(hit.source()); -} +include-tagged::{doc-tests}/ConnectingTest.java[first-request] -------------------------------------------------- diff --git a/docs/index-local.asciidoc b/docs/index-local.asciidoc new file mode 100644 index 000000000..48b04093a --- /dev/null +++ b/docs/index-local.asciidoc @@ -0,0 +1,4 @@ +// Allow building docs locally without a checkout of the Elasticsearch repo +:elasticsearch-root: {docdir}/local/elasticsearch + +include::index.asciidoc[] diff --git a/docs/index.asciidoc b/docs/index.asciidoc index da2315a00..9eec09a04 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -3,9 +3,18 @@ :branch: master include::{asciidoc-dir}/../../shared/attributes.asciidoc[] +:version: 7.16.0 +:java-client: Java API Client +:doc-tests: {docdir}/../java-client/src/test/java/co/elastic/clients/documentation + include::introduction.asciidoc[] include::installation.asciidoc[] include::connecting.asciidoc[] include::migrate.asciidoc[] include::api-conventions.asciidoc[] -include::{elasticsearch-root}/docs/java-rest/low-level/index.asciidoc[] \ No newline at end of file + +:version!: +:java-client!: +:doc-tests!: + +include::{elasticsearch-root}/docs/java-rest/low-level/index.asciidoc[] diff --git a/docs/installation.asciidoc b/docs/installation.asciidoc index 2746cf224..1ec3a6507 100644 --- a/docs/installation.asciidoc +++ b/docs/installation.asciidoc @@ -1,21 +1,19 @@ [[installation]] == Installation -beta[] - Requirements: * Java 8 or later. -* Optionally, a JSON object mapping library to allow seamless integration of - your application classes with the Elasticsearch API. The Java client has - support for https://github.com/FasterXML/jackson[Jackson] or a +* A JSON object mapping library to allow seamless integration of + your application classes with the Elasticsearch API. The Java client has + support for https://github.com/FasterXML/jackson[Jackson] or a http://json-b.net/[JSON-B] library like https://github.com/eclipse-ee4j/yasson[Eclipse Yasson]. -Releases are hosted on -https://search.maven.org/search?q=g:co.elastic.clients[Maven Central]. If you -are looking for a SNAPSHOT version, the Elastic Maven Snapshot repository is +Releases are hosted on +https://search.maven.org/search?q=g:co.elastic.clients[Maven Central]. If you +are looking for a SNAPSHOT version, the Elastic Maven Snapshot repository is available at https://snapshots.elastic.co/maven/. @@ -23,10 +21,10 @@ available at https://snapshots.elastic.co/maven/. [[gradle]] === Installation in a Gradle project by using Jackson -["source","groovy",subs="attributes"] +["source","groovy",subs="attributes+"] -------------------------------------------------- dependencies { - implementation 'co.elastic.clients:elasticsearch-java:7.15.0' + implementation 'co.elastic.clients:elasticsearch-java:{version}' implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3' } -------------------------------------------------- @@ -38,7 +36,7 @@ dependencies { In the `pom.xml` of your project, add the following repository definition and dependencies: -["source","xml",subs="attributes"] +["source","xml",subs="attributes+"] -------------------------------------------------- @@ -46,7 +44,7 @@ dependencies: co.elastic.clients elasticsearch-java - 7.15.0 + {version} com.fasterxml.jackson.core diff --git a/docs/introduction.asciidoc b/docs/introduction.asciidoc index 912d8e0ae..466efca3f 100644 --- a/docs/introduction.asciidoc +++ b/docs/introduction.asciidoc @@ -1,8 +1,6 @@ [[introduction]] == Introduction -beta[] - This is the documentation for the official Java API Client for {es}. The client provides strongly typed requests and responses for all {es} APIs. @@ -20,29 +18,32 @@ provides strongly typed requests and responses for all {es} APIs. retries, node discovery, and so on. [discrete] -=== Beta status - -The Java API Client is a new product and is still in beta. What this means in -concrete terms: - -* Although the general structure will not change, some renaming may happen to - improve ease of use. -* Some APIs may not be implemented yet. The code for APIs is generated from the - https://github.com/elastic/elasticsearch-specification[{es} API specification] - where complete coverage is targeted for the GA release of the Java client. -* Some complex API data types are not yet fully supported. This revolves mostly - around union types where the JSON API accepts many different shapes for some - property. These types are currently represented as raw `JsonValue` objects. -* Some built-in types like dates are represented as primitive strings. - Additional support for standard JDK types will be added for the GA release. +=== Main changes since version 7.15 + +Version 7.15 of the {java-client} was a beta release. We learned a lot from the +initial feedback and this led to a few breaking changes in this version: + +* the `_core` package that contains top-level {es} APIs has been renamed to `core`. +* property setters for lists and maps have been revisited. +* all API data types are now supported, and there are no more raw `JsonValue` properties. +* the supporting JSON framework and transport classes have been refactored and improved. + However the high level APIs used by applications haven't changed so there should be + no of very little impact on existing application code. + +This version of the {java-client} supports all {es} APIs are implemented except the +Vector tile search API and the Find structure API. + +While this version is considered stable, most of the code is generated from the +https://github.com/elastic/elasticsearch-specification[{es} API specification] where +some of the less often used {es} APIs still need to be fully validated. Changes in +the specification that may impact application code will be clearly identified in later +versions of the Java API client. [discrete] === Compatibility -The main branch targets the next major release (8.0), the 7.x branch targets the -next minor release for the 7.x series. Support is still incomplete as the API -code is generated from the {es} Specification that is also still a work in -progress. +The `main` branch targets the next major release (8.0), the `7.x` branch targets the +next minor release for the `7.x` series. The {es} Java client is forward compatible; meaning that the client supports communicating with greater or equal minor versions of {es}. {es} language diff --git a/docs/local/README.md b/docs/local/README.md new file mode 100644 index 000000000..a68e64521 --- /dev/null +++ b/docs/local/README.md @@ -0,0 +1 @@ +This directory contains stubs that allow building the Java client docs in isolation following the instructions in "[Building documentation for a local repo](https://github.com/elastic/docs#building-documentation)". diff --git a/docs/local/elasticsearch/docs/java-rest/low-level/index.asciidoc b/docs/local/elasticsearch/docs/java-rest/low-level/index.asciidoc new file mode 100644 index 000000000..c7b8560f3 --- /dev/null +++ b/docs/local/elasticsearch/docs/java-rest/low-level/index.asciidoc @@ -0,0 +1,4 @@ +[[java-rest-low]] +== Java Low Level REST Client + +This is a stub for the Java Low Level REST Client. diff --git a/docs/migrate.asciidoc b/docs/migrate.asciidoc index f531d30fd..af48bc17c 100644 --- a/docs/migrate.asciidoc +++ b/docs/migrate.asciidoc @@ -39,21 +39,5 @@ The code below shows how to initialize both clients with the same HTTP client: ["source","java"] -------------------------------------------------- -// Create the low-level client -RestClientBuilder httpClientBuilder = RestClient.builder( - new HttpHost("localhost", 9200) -); - -// Create the HLRC -RestHighLevelClient hlrc = new RestHighLevelClient(httpClientBuilder); - -// Create the new Java Client with the same low level client -Transport transport = new RestClientTransport( - hlrc.getLowLevelClient(), - new JacksonJsonpMapper() -); - -ElasticsearchClient esClient = new ElasticsearchClient(transport); - -// hlrc and esClient share the same httpClient +include-tagged::{doc-tests}/MigrateHlrcTest.java[migrate] -------------------------------------------------- diff --git a/java-client/src/test/java/co/elastic/clients/documentation/ApiConventionsTest.java b/java-client/src/test/java/co/elastic/clients/documentation/ApiConventionsTest.java index 8c66b9ce6..f043b3d7d 100644 --- a/java-client/src/test/java/co/elastic/clients/documentation/ApiConventionsTest.java +++ b/java-client/src/test/java/co/elastic/clients/documentation/ApiConventionsTest.java @@ -21,6 +21,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient; import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.NodeStatistics; import co.elastic.clients.elasticsearch._types.SortOrder; import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; import co.elastic.clients.elasticsearch._types.query_dsl.Query; @@ -31,6 +32,7 @@ import co.elastic.clients.elasticsearch.indices.CreateIndexResponse; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.TransportException; +import co.elastic.clients.util.ApiTypeHelper; import org.junit.Assert; import org.junit.Test; @@ -234,6 +236,25 @@ public void collections() { } + @Test + public void optionalCollections() { + //tag::optional-collections + NodeStatistics stats = NodeStatistics.of(b -> b + .total(1) + .failed(0) + .successful(1) + ); + + // The `failures` list was not provided. + // - it's not null + assertNotNull(stats.failures()); + // - it's empty + assertEquals(0, stats.failures().size()); + // - and if needed we can know it was actually not defined + assertFalse(ApiTypeHelper.isDefined(stats.failures())); + //end::optional-collections + } + private void doSomething(Object... o) { }