org.instancio.BaseApi Maven / Gradle / Ivy
Show all versions of instancio-core Show documentation
/*
* Copyright 2022-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.instancio;
import org.instancio.documentation.ExperimentalApi;
import org.instancio.exception.InstancioApiException;
import org.instancio.feed.Feed;
import org.instancio.feed.FeedProvider;
import org.instancio.generator.AfterGenerate;
import org.instancio.generator.Generator;
import org.instancio.generator.GeneratorSpec;
import org.instancio.generators.Generators;
import org.instancio.settings.Keys;
import org.instancio.settings.Settings;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/**
* Defines top-level operations supported by the API.
*
* @param the type of object to create
* @since 4.0.0
*/
interface BaseApi {
/**
* Specifies that a class or field should be ignored.
*
* Example:
*
{@code
* Person person = Instancio.of(Person.class)
* .ignore(field(Phone::getPhoneNumber))
* .ignore(allStrings())
* .create();
* }
*
* will create a fully populated person, but will ignore the
* {@code getPhoneNumber} field, and all strings.
*
*
Precedence
*
* This method has higher precedence than other API methods.
* Once a target is ignored, no other selectors will apply.
* For example, the following snippet will trigger an unused selector error
* because {@code field(Phone::getNumber)} is redundant:
*
*
{@code
* Person person = Instancio.of(Person.class)
* .ignore(all(Phone.class))
* .set(field(Phone::getNumber), "123-45-56")
* .create();
* }
*
* Usage with Java records
*
* If {@code ignore()} targets one of the required arguments of a record
* constructor, then a default value for the ignored type will be generated.
*
*
Example:
*
{@code
* record PersonRecord(String name, int age) {}
*
* PersonRecord person = Instancio.of(PersonRecord.class)
* .ignore(allInts())
* .ignore(allStrings())
* .create();
*
* // will produce: PersonRecord[name=null, age=0]
* }
*
* @param selector for fields and/or classes this method should be applied to
* @return API builder reference
* @since 4.0.0
*/
BaseApi ignore(TargetSelector selector);
/**
* Specifies that a field or class is nullable. By default, Instancio assigns
* non-null values to fields. If marked as nullable, Instancio will generate either
* a null or non-null value.
*
* Example:
*
{@code
* Person person = Instancio.of(Person.class)
* .withNullable(allStrings())
* .withNullable(field(Person::getAddress))
* .withNullable(fields().named("lastModified"))
* .create();
* }
*
* Note: a type marked as nullable using this method is only nullable
* when declared as a field, but not as a collection element,
* or map key/value. For example, {@code withNullable(allStrings())}
* will not generate nulls in a {@code List}.
*
* @param selector for fields and/or classes this method should be applied to
* @return API builder reference
*/
BaseApi withNullable(TargetSelector selector);
/**
* Sets a value to matching selector targets.
*
* Example: if {@code Person} class contains a {@code List}, the following
* snippet will set all the country code of all phone instances to "+1".
* {@code
* Person person = Instancio.of(Person.class)
* .set(field(Phone::getCountryCode), "+1")
* .create();
* }
*
* Note: Instancio will not
*
* - populate or modify objects supplied by this method
* - apply other {@code set()}, {@code supply()}, or {@code generate()}}
* methods with matching selectors to the supplied object
* - invoke {@code onComplete()} callbacks on supplied instances
*
*
* @param selector for fields and/or classes this method should be applied to
* @param value value to set
* @param type of the value
* @return API builder reference
* @see #supply(TargetSelector, Supplier)
* @since 4.0.0
*/
BaseApi set(TargetSelector selector, V value);
/**
* Applies given {@code model} to the specified {@code selector}.
*
* For example, given the following classes and {@link Model}:
*
*
{@code
* record Foo(String value) {}
* record Container(Foo fooA, Foo fooB) {}
*
* Model fooModel = Instancio.of(Foo.class)
* .set(field(Foo::value), "foo")
* .toModel();
* }
*
* The model can be applied to a specific {@code Foo} field declared
* by the {@code Container}:
*
*
{@code
* Container container = Instancio.of(Container.class)
* .setModel(field(Container::fooA), fooModel)
* .create();
* }
*
* Alternatively, to apply the model to all instances of {@code Foo}:
*
*
{@code
* Container container = Instancio.of(Container.class)
* .setModel(all(Foo.class), fooModel)
* .create();
* }
*
* Note: the following properties of the supplied model
* are not applied to the target object:
*
*
* - {@link Settings}
* - {@code lenient()} mode
* - custom seed value
*
*
* See the
* user guide
* for further details.
*
* @param selector to which the model will be applied to
* @param model to apply to the given selector's target
* @param the type of object this model represents
* @return API builder reference
* @since 4.4.0
*/
@ExperimentalApi
BaseApi setModel(TargetSelector selector, Model model);
/**
* Supplies an object using a {@link Supplier}.
*
* Example:
*
{@code
* Person person = Instancio.of(Person.class)
* .supply(all(LocalDateTime.class), () -> LocalDateTime.now())
* .supply(field(Address::getPhoneNumbers), () -> List.of(
* new PhoneNumber("+1", "123-45-67"),
* new PhoneNumber("+1", "345-67-89")))
* .create();
* }
*
* Note: Instancio will not
*
* - populate or modify objects supplied by this method
* - apply other {@code set()}, {@code supply()}, or {@code generate()}}
* methods with matching selectors to the supplied object
* - invoke {@code onComplete()} callbacks on supplied instances
*
*
* If you require the supplied object to be populated and/or selectors
* to be applied, use the {@link #supply(TargetSelector, Generator)} method instead.
*
* @param selector for fields and/or classes this method should be applied to
* @param supplier providing the value for given selector
* @param type of the supplied value
* @return API builder reference
* @see #supply(TargetSelector, Generator)
* @since 4.0.0
*/
BaseApi supply(TargetSelector selector, Supplier supplier);
/**
* Supplies an object using a {@link Generator} to matching selector targets.
* By default, Instancio will populate uninitialised fields of the supplied
* object. This includes fields with {@code null} or default primitive values.
*
* This method supports the following use cases.
*
*
Generate random objects
*
* This method provides an instance of {@link Random} that can be used
* to randomise generated objects. For example, if Instancio did not support
* creation of {@code java.time.Year}, it could be generated as follows:
*
*
{@code
* List years = Instancio.ofList(Year.class)
* .supply(all(Year.class), random -> Year.of(random.intRange(1900, 2000)))
* .create();
* }
*
* Provide a partially initialised instance
*
* In some cases, an object may need to be created in a certain state or
* instantiated using a specific constructor to be in a valid state.
* A partially initialised instance can be supplied using this method, and
* Instancio will populate remaining fields that are {@code null}:
*
*
{@code
* Person person = Instancio.of(Person.class)
* .supply(field(Person::getAddress), random -> new Address("Springfield", "USA"))
* .create();
* }
*
* This behaviour is controlled by the {@link AfterGenerate} hint specified
* by {@link Generator#hints()}. Refer to the {@link Generator#hints()} Javadoc
* for details, or
* Custom Generators section of the user guide.
*
* @param selector for fields and/or classes this method should be applied to
* @param generator that will provide the values
* @param type of the value to generate
* @return API builder reference
* @see Generator
* @see AfterGenerate
* @see Keys#AFTER_GENERATE_HINT
* @since 4.0.0
*/
BaseApi supply(TargetSelector selector, Generator generator);
/**
* Customises values using built-in generators provided by the {@code gen}
* parameter, of type {@link Generators}.
*
* Example:
*
{@code
* Person person = Instancio.of(Person.class)
* .generate(field(Person::getAge), gen -> gen.ints().range(18, 100))
* .generate(all(LocalDate.class), gen -> gen.temporal().localDate().past())
* .generate(field(Address::getPhoneNumbers), gen -> gen.collection().size(5))
* .generate(field(Address::getCity), gen -> gen.oneOf("Burnaby", "Vancouver", "Richmond"))
* .create();
* }
*
* @param selector for fields and/or classes this method should be applied to
* @param gen provider of customisable built-in generators (also known as specs)
* @param type of object to generate
* @return API builder reference
* @see #generate(TargetSelector, GeneratorSpec)
* @see Generators
*/
BaseApi generate(TargetSelector selector, GeneratorSpecProvider gen);
/**
* Customises values using arbitrary generator specs.
*
* Example:
*
{@code
* Person person = Instancio.of(Person.class)
* .generate(field(Person::getAge), Instancio.ints().range(18, 100))
* .generate(all(LocalDate.class), Instancio.temporal().localDate().past())
* .generate(field(Phone::getNumber), MyCustomGenerators.phones().northAmerican())
* .create();
* }
*
* @param selector for fields and/or classes this method should be applied to
* @param spec generator spec
* @param type of object to generate
* @return API builder reference
* @see #generate(TargetSelector, GeneratorSpecProvider)
* @since 4.0.0
*/
BaseApi generate(TargetSelector selector, GeneratorSpec spec);
/**
* A callback that gets invoked after the root object has been fully populated.
*
* Example:
*
{@code
* // Sets countryCode field on all instances of Phone to the specified value
* Person person = Instancio.of(Person.class)
* .onComplete(all(Phone.class), (Phone phone) -> phone.setCountryCode("+1"))
* .create();
* }
*
* Note: callbacks are never invoked on objects provided using:
*
*
* - {@link #set(TargetSelector, Object)}
* - {@link #supply(TargetSelector, Supplier)}
*
*
* {@code
* OnCompleteCallback callback = (String value) -> {
* // do something with the value
* };
*
* Person person = Instancio.of(Person.class)
* .set(field(Phone::getCountryCode), "+1")
* // The callback will not be invoked because
* // Phone.countryCode value was provided via set() method
* .onComplete(field(Phone::getCountryCode), callback)
* .create();
* }
*
* @param selector for fields and/or classes this method should be applied to
* @param callback to invoke after object has been populated
* @param type of object handled by the callback
* @return API builder reference
* @since 4.0.0
*/
BaseApi onComplete(TargetSelector selector, OnCompleteCallback callback);
/**
* Filters generated values using given {@code predicate}.
* If a value is rejected, a new value will be generated,
* which will also be tested against the {@code predicate}.
* If no value is accepted after {@link Keys#MAX_GENERATION_ATTEMPTS},
* an exception will be thrown.
*
* A simple example is to generate a list of even numbers:
*
{@code
* List evenNumbers = Instancio.ofList(Integer.class)
* .filter(allInts(), (Integer i) -> i % 2 == 0)
* .create();
* }
*
* Note that customising objects using this method is less efficient
* than {@link #generate(TargetSelector, GeneratorSpecProvider)}.
* The latter should be preferred where possible.
*
* @param selector for fields and/or classes this method should be applied to
* @param predicate that must be satisfied by the generated value
* @param the type of object the predicate is evaluated against
* @return API builder reference
* @see Keys#MAX_GENERATION_ATTEMPTS
* @since 4.6.0
*/
@ExperimentalApi
BaseApi filter(TargetSelector selector, FilterPredicate predicate);
/**
* Maps target field or class to the given subtype. This can be used
* in the following cases:
*
*
* - to specify an implementation for interfaces or abstract classes
* - to override default implementations used by Instancio
*
* Specify an implementation for an abstract type
*
* When Instancio encounters an interface or an abstract type it is not aware of
* (for example, that is not part of the JDK), it will not be able to instantiate it.
* This method can be used to specify an implementation to use in such cases.
* For example:
*
*
{@code
* WidgetContainer container = Instancio.of(WidgetContainer.class)
* .subtype(all(AbstractWidget.class), ConcreteWidget.class)
* .create();
* }
*
* Override default implementations
*
* By default, Instancio uses certain defaults for collection classes, for example
* {@link ArrayList} for {@link List}.
* If an alternative implementation is required, this method allows to specify it:
*
*
{@code
* Person person = Instancio.of(Person.class)
* .subtype(all(List.class), LinkedList.class)
* .create();
* }
*
* will use the {@code LinkedList} implementation for all {@code List}s.
*
* @param selector for fields and/or classes this method should be applied to
* @param subtype to map the selector to
* @return API builder reference
* @since 4.0.0
*/
BaseApi subtype(TargetSelector selector, Class> subtype);
/**
* Generates values based on given assignments.
* An {@link Assignment} can be created using one of the builder patterns
* provided by the {@link Assign} class.
*
*
* - {@code Assign.valueOf(originSelector).to(destinationSelector)}
* - {@code Assign.given(originSelector).satisfies(predicate).set(destinationSelector, value)}
* - {@code Assign.given(originSelector, destinationSelector).set(predicate, value)}
*
*
* For example, the following snippet uses
* {@link Assign#given(TargetSelector, TargetSelector)} to create
* an assignment that sets {@code Phone.countryCode} based on
* the value of the {@code Address.country} field:
*
*
{@code
* Assignment assignment = Assign.given(field(Address::getCountry), field(Phone::getCountryCode))
* .set(When.isIn("Canada", "USA"), "+1")
* .set(When.is("Italy"), "+39")
* .set(When.is("Poland"), "+48")
* .set(When.is("Germany"), "+49");
*
* Person person = Instancio.of(Person.class)
* .generate(field(Address::getCountry), gen -> gen.oneOf("Canada", "USA", "Italy", "Poland", "Germany"))
* .assign(assignment)
* .create();
* }
*
* The above API allows specifying different values for a given
* origin/destination pair. An alternative for creating a conditional
* is provided by {@link Assign#given(TargetSelector)}. This method
* allows specifying different destination selectors for a given origin:
*
*
{@code
* Assignment shippedOrderAssignment = Assign.given(Order::getStatus)
* .is(OrderStatus.SHIPPED)
* .supply(field(Order::getDeliveryDueDate), () -> LocalDate.now().plusDays(2));
*
* Assignment cancelledOrderAssignment = Assign.given(Order::getStatus)
* .is(OrderStatus.CANCELLED)
* .set(field(Order::getCancellationReason), "Shipping delays")
* .generate(field(Order::getCancellationDate), gen -> gen.temporal().instant().past());
*
* List orders = Instancio.ofList(Order.class)
* .generate(all(OrderStatus.class), gen -> gen.oneOf(OrderStatus.SHIPPED, OrderStatus.CANCELLED))
* .assign(shippedOrderAssignment, cancelledOrderAssignment)
* .create();
* }
*
* Limitations of assignments
*
* Using assignments has a few limitations to be aware of.
*
*
* - The origin selector must match a single target.
* It must not be a {@link SelectorGroup} created via
* {@link Select#all(GroupableSelector...)} or primitive/wrapper
* selector, such as {@link Select#allInts()}
* - An assignment where the origin selector's target is within
* a collection element must have a destination selector
* within the same collection element.
* - Circular assignments will produce an error.
*
*
* @param assignments one or more assignment expressions for setting values
* @return API builder reference
* @throws InstancioApiException if the origin selector of an assignment
* matches more than one target, or the
* assignments form a cycle
* @see Assign
* @since 4.0.0
*/
BaseApi assign(Assignment... assignments);
/**
* Applies the provided {@code feed} to the specified {@code selector}.
* The {@code selector} targets must be POJOs or Java {@code record}s.
* Properties from the {@code feed} will be automatically mapped to the selected objects.
*
* For example, we can generate instances of the following record:
*
*
{@code
* record Person(String firstName, String lastName, String fullName,
* int age, String username, String email) {}
* }
*
* using data from a CSV file (formatted for readability):
*
*
* firstName, lastName, age, username
* John, Doe, 24, john_doe
* Alice, Smith, 55, alice_s
* # more entries...
*
*
* by defining the following feed:
*
*
{@code
* @Feed.Source(resource = "persons.csv")
* interface PersonFeed extends Feed {
*
* @TemplateSpec("${firstName} ${lastName}")
* FeedSpec fullName();
*
* @GeneratedSpec(CustomEmailGenerator.class)
* FeedSpec email();
* }
* }
*
* and applying the feed using the {@code all(Person.class)} selector:
*
*
{@code
* Feed personFeed = Instancio.createFeed(PersonFeed.class);
*
* List persons = Instancio.ofList(Person.class)
* .applyFeed(all(Person.class), personFeed)
* .create();
* }
*
* Data from the CSV will be mapped to {@code Person} fields
* by matching field names to property names in the data file.
*
*
* - Note that {@code PersonFeed} does not need to declare
* {@code firstName()} and {@code lastName()} methods
* (they are mapped automatically).
* - {@code fullName()} and {@code email()} can also
* be mapped to the {@code Person} object, even though
* these are generated and not present in the data file.
*
*
* @param selector for fields and/or classes this method should be applied to
* @param feed the feed to apply to the selector
* @return API builder reference
* @see Instancio#createFeed(Class)
* @see #applyFeed(TargetSelector, FeedProvider)
* @since 5.0.0
*/
@ExperimentalApi
BaseApi applyFeed(TargetSelector selector, Feed feed);
/**
* Creates a feed and applies it to the specified {@code selector}.
* The selector's target must be a POJO or a Java {@code record}.
* Properties from the feed will be automatically mapped to the
* selected object.
*
* For example, given the following CSV file (formatted for readability):
*
*
* firstName, lastName, age, username
* John, Doe, 24, john_doe
* Alice, Smith, 55, alice_s
* # more entries...
*
*
* and a record with matching properties:
*
*
{@code
* class Person(String firstName, String lastName, int age, String username) {}
* }
*
* a feed can be applied as follows:
*
*
{@code
* List persons = Instancio.ofList(Person.class)
* .applyFeed(all(Person.class), feed -> feed.ofResource("data/persons.csv"))
* .create();
*
* // Output:
* // [Person[firstName=John, lastName=Doe, age=24, username=john_doe],
* // Person[firstName=Alice, lastName=Smith, age=55, username=alice_s]]
* }
*
* @param selector for fields and/or classes this method should be applied to
* @param provider the provider API for specifying feed configuration
* @return API builder reference
* @see #applyFeed(TargetSelector, Feed)
* @since 5.0.0
*/
@ExperimentalApi
BaseApi applyFeed(TargetSelector selector, FeedProvider provider);
/**
* Specifies the maximum depth for populating an object.
* The root object is at depth zero. Children of the root
* object are at depth 1, grandchildren at depth 2, and so on.
*
* Instancio will populate values up to the maximum depth.
* Beyond that, values will be {@code null} unless the maximum
* depth is set to a higher value.
*
*
The default maximum depth is defined by {@link Keys#MAX_DEPTH}.
*
*
Note: this method is a shorthand for:
*
*
{@code
* int maxDepth = 5;
* Person person = Instancio.of(Person.class)
* .withSettings(Settings.create().set(Keys.MAX_DEPTH, maxDepth))
* .create();
* }
*
* If the maximum depth is specified using {@code Settings} and
* this method, then this method takes precedence.
*
* @param maxDepth the maximum depth, must not be negative
* @return API builder reference
* @since 4.0.0
*/
BaseApi withMaxDepth(int maxDepth);
/**
* Sets the seed value for the random number generator. If the seed is not specified,
* a random seed will be used. Specifying the seed is useful for reproducing test results.
* By specifying the seed value, the same random data will be generated again.
*
*
* Example:
*
{@code
* // Generates a different UUID each time
* UUID result = Instancio.create(UUID.class);
*
* // Generates the same UUID each time
* UUID result = Instancio.of(UUID.class)
* .withSeed(1234)
* .create();
* }
*
* @param seed for the random number generator
* @return API builder reference
* @since 4.0.0
*/
BaseApi withSeed(long seed);
/**
* Specifies that a blank object should be generated for the selected target.
*
* A blank object has the following properties:
*
*
* - value fields (strings, numbers, dates, etc) are {@code null}
* - arrays, collections, and maps are empty
* - nested POJOs are blank
*
*
* Example:
*
{@code
* Person person = Instancio.of(Person.class)
* .setBlank(field(Person::getAddress))
* .create();
*
* // Output:
* // Person[
* // name="GNQTXA",
* // dateOfBirth=1988-04-09,
* // address=Address[street=null, city=null, country=null] // blank Address
* //]
* }
*
* @param selector for fields and/or classes this method should be applied to
* @return API builder reference
* @see Instancio#createBlank(Class)
* @see Instancio#ofBlank(Class)
* @since 4.7.0
*/
@ExperimentalApi
BaseApi setBlank(TargetSelector selector);
/**
* Specifies that the given selector's target(s) should have unique values.
*
* Example:
*
{@code
* record Data(int foo, int bar) {}
*
* List results = Instancio.ofList(Data.class)
* .size(100)
* .withUnique(field(Data::foo))
* .create();
* }
*
* The above snippet generates a list of {@code Data} instances
* with unique {@code foo} values. Note that values will be unique
* across all targets that match the selector. For instance,
* the following usages:
*
*
* - {@code withUnique(allInts())}
* - {@code withUnique(all(field(Data::foo), field(Data::bar))}
*
*
* would result in unique values for {@code foo} and {@code bar}
* with no overlap (i.e. {@code foo} and {@code bar} are disjoint).
* To generate unique values per field (with potential overlap),
* the {@code withUnique()} method must be specified per field:
*
*
{@code
* List results = Instancio.ofList(Data.class)
* .size(100)
* .withUnique(field(Data::foo)) // e.g. { 601, 42, 573, ...}
* .withUnique(field(Data::bar)) // e.g. { 888, 251, 42, ...}
* .create();
* }
*
* If it is impossible to generate a sufficient number of
* unique values after a certain number of attempts,
* an exception will be thrown:
*
*
{@code
* List results = Instancio.ofList(Boolean.class)
* .size(10) // will fail as it's impossible to generate 10 unique booleans
* .withUnique(allBooleans())
* .create();
* }
*
* @param selector for fields and/or classes this method should be applied to
* @return API builder reference
* @since 4.8.0
*/
@ExperimentalApi
BaseApi withUnique(TargetSelector selector);
}