|
User Generator 2.1.2
Synthetic user records for apps and test data, composed from the other dasmig generators for C++23
|
This guide covers every feature of the user-generator library. For a quick overview, see the README. For the full API reference, run doxygen Doxyfile from the repository root and open doc/api/html/index.html.
Copy dasmig/usergen.hpp, dasmig/core.hpp and the dasmig/core/ directory into your include path, keeping them together. The libraries it composes must be on the include path too: address, birth, country, email, name, nickname and phone, each with its own data, and device, whose data is needed only for users with a device (user_request::device). Supply resources/ next to the program, or define DASMIG_USER_EMBED to compile the account tables in; the other libraries have their own DASMIG_<NAME>_EMBED macros for their lite tiers.
user-generator does not need persona-generator, and a user carries no body, traits, interests, job or city.
instance() loads the account tables' lite tier on first use (or full if there is no lite), from the compiled-in copy or from the first of resources/, ../resources, user-generator/resources and ../user-generator/resources that has it. The libraries it composes load their own the same way, through their singletons.
load loads the account tables only; it replaces them and every layer loaded before. To load a tier into a library it composes, call that library's own load:
A usg draws from the singletons of the seven libraries by default, and from device-generator's for a user with a device (sources::device, null by default, means dvg::instance(), which loads its data on the first such user). Construct one over your own instances to use different data side by side:
Every field of a user_request is optional, and what is left out is drawn:
| Field | Meaning | Default |
|---|---|---|
country | ISO 3166-1 alpha-2 code, any case | one of supported_countries(), by population |
person_sex | dasmig::sex::male or female | drawn with the birth |
ages | age_range{.min, .max} in completed years | 18 to 80 |
reference_date | the date ages and account times are computed at | birth-generator's, 2023-07-01 (the clock is never read) |
safe | contacts that reach no one (see below) | false |
password | a password_policy | 12 to 16 characters, all four classes |
sign_up_age | the youngest age at which a person signs up | 13 |
service_start | the earliest date an account can be created | ten years before the reference date |
status | an account status key | drawn |
device | add the device of the last sign-in (see below) | false |
ages.min must be at least sign_up_age; lower both for a service for teenagers.
The fields of u.account:
| Field | Value |
|---|---|
username | nickname-generator's username style, from the name and the birth year (iam_ycarvalho63) |
password | to the request's policy, plain text |
created | sign-up time, ISO 8601 UTC (2019-02-05T07:32:31Z) |
last_login | ISO 8601 UTC; empty if the account never logged in |
status, status_label | active, inactive, unverified or suspended in the shipped data |
platform, platform_label, device, os | for example ios-phone, iPhone, phone, iOS |
locale | BCP 47 (pt-BR) |
timezone | IANA name (America/Sao_Paulo), or a UTC offset (UTC+03:00) where no zone is listed |
currency | ISO 4217 code of the country (BRL) |
Each part follows the country: the name's culture, the address's layout, the phone's numbering plan, the email's provider mix and style, the locale, the time zone and the currency. The username and the email are built from the name and the birth year, so they resemble each other.
password_policy sets the length and the character classes:
The length is drawn uniformly between the bounds. With require_each (the default) the password has at least one character of every allowed class, so min_length must be at least the number of classes; without it the characters are drawn from all allowed classes alike. The symbols are !@#$%^&*-_=+?. Passwords are plain text and test data. By default they are random strings, which look nothing like what people choose.
style = password_style::human builds them the way people do instead: a pattern from password_patterns.tsv filled in with common words, the person's given name, their birth year, numbers such as 123 or 69, and a symbol (mostly !), so you get Dragon123, Isabel1987!, sunshine, p@ssw0rd, qwerty123 or Monkey2009. The word and pattern shares are in password_words.tsv and password_patterns.tsv (see Data and Method); extend them like the other tables. The result is then bent to the policy, as a sign-up form would: characters the rules do not allow are dropped (a letter changes case if only one case is allowed), the digit or the symbol that is missing is added, it is cut to max_length and padded to min_length. With the default rules (12 to 16 characters, all four classes) most passwords end up with padding, so for a mix closer to real lists ask for looser ones:
The random style is the default and does not change with the data, so the seeds of earlier releases give the same passwords. The style only draws the password: every other part of the user stays the same. A generator whose password_patterns table is empty throws std::runtime_error for the human style.
An account is created after the person reaches sign_up_age and after service_start, and before the reference date. Sign-ups grow towards the reference date, so most accounts are recent. The status then places the last login:
login of the status | Last login |
|---|---|
recent | within the 90 days before the reference date (or after the sign-up, if later), most very recent |
stale | a year or more before the reference date, after the sign-up (the sign-up itself for an account younger than a year) |
never | none: last_login is empty, and null in the JSON |
Forcing the status changes nothing else about the user: every part draws from its own stream.
With safe, the email uses a reserved domain (example.com, .net, .org, RFC 2606) and the phone number a number from the range regulators set aside for fiction, where the country has one (the US and Canada 555-01xx, the UK 07700 900xxx, and a few more); mail.reserved and mobile.reserved say so. In a country with no such range the phone number is outside the allocated ranges instead, and reserved is false.
The address is not changed by safe: its street is a real name with a composed number, which is not a person's address, but it can match one by chance.
With request.device, a user also has client, the device of the last sign-in, from device-generator: a model, OS and browser version that fit together at a date, the User-Agent header and client hints that combination sends, an Accept-Language header, a screen and an IP address in the country.
The device is device-generator's get_device for the user's country, the account's platform (so client->kind and client->os are the account's device and os) and locale, at the day of the last login, or of the sign-up for an account that never logged in. device-generator covers dates from 2021-01-01, so an earlier login is seen at that date. With safe, the IP address is from a documentation range (RFC 5737, RFC 3849) and client->reserved is true. It draws from the user's "device" stream, so it is get_device(that request, core::rng{seed}.split("device")()), and turning it on changes nothing else in the user. A drawn country is one device-generator has data for; every country of the shipped data has.
The device is off by default, so users without it, their JSON and their signatures are those of releases before it. device-generator's header must be on the include path either way (usergen.hpp includes it); its data is loaded only by the first user with a device.
get_user(request, seed) gives the same result for the same request, seed and data, on every platform, without advancing the instance engine.get_user(request) draws a fresh seed from the instance engine, initially seeded from system entropy. There are no generator-level seed() or unseed() controls; for deterministic sequences, keep a dasmig::core::rng in the caller and pass its next value as the seed.u.seed() is the seed a result was generated from: get_user(request, u.seed()) replays it with the same request and data.country, birth, name, username, email, phone, address, password, dates, status, platform, locale, timezone) draws from a stream split from the seed by that name, so forcing one choice, or changing the password policy, does not shift the others. The one exception is the country: when it is drawn, the same seed can end on a different country if the data of a library changes.The shipped account tables are base layers. Files with the same columns can be loaded on top of them to add rows, change them or remove them, without editing the shipped files (see Data and Method for the columns). A table is named by dasmig::user_table: platforms, platform_shares, statuses, languages, locales, timezones, password_words and password_patterns.
dasmig::core::tsv_error with the file, line and column, and the data stays as it was. A layer for a directory is applied to all its tables or to none.layers(table) lists what is loaded in a table: source, mode, rows added, replaced and removed, and warnings such as unknown columns.load(...) replaces the base and drops every layer.extend; the user's data_hash() follows.Every result records the seed and the data it came from:
to_json() gives the result as JSON. Each library's result is written as that library writes it, next to the user's own fields:
A user with a device has "device":{...} after "account", as device-generator writes it.
Seeds are strings because JavaScript numbers cannot hold every 64-bit value. to_json(writer) writes into an existing dasmig::core::json_writer, for composing larger documents.
Define DASMIG_USER_EMBED before including the header to compile the account tables' lite tier into the program. load(dataset::lite) and instance() then use it and need no files. The compiled-in data has the same data_hash() as the files, so seeds replay the same either way.
dasmig/usergen_data.hpp is generated from resources/lite by make embed; CI checks it is up to date. Define the other libraries' macros too (DASMIG_ADDRESS_EMBED, DASMIG_BIRTH_EMBED, DASMIG_COUNTRY_EMBED, DASMIG_DEVICE_EMBED, DASMIG_EMAIL_EMBED, DASMIG_NAME_EMBED, DASMIG_NICKNAME_EMBED, DASMIG_PHONE_EMBED) for a build without files.
get_user(request, seed) is const and only calls const members of the libraries, so it may run concurrently on one instance, which is what the batch calls do. get_user(request) draws from the instance engine, and load() and extend() change the tables: both need external synchronisation (e.g. a std::mutex) on the same instance. The libraries' singletons follow their own rules.
Every seeded getter has batch forms: get_user_batch returns a std::vector, and for_each_user streams the items to a sink in index order without holding the batch.
i is exactly get_user(request, core::item_seed(batch_seed, first + i)). A range can be generated directly (first = 1000 starts at item 1000), two batches that follow each other equal one, and the output is the same for any threads (0 means the hardware count). Without a batch_seed one is drawn like the single getter draws its own.core::batch_options has count, first, threads and unique. The request describes one item; the options describe the batch..unique = core::unique_rule::by<user>(key) regenerates an item whose key already appeared in the batch, with a deterministic retry seed, and throws std::runtime_error naming the item when the attempts run out. Unique by the email address, the username or the phone number is the usual choice; with one country the number of distinct values bounds the batch.core::jsonl_writer writes one to_json() per line, core::csv_writer flattens each result's JSON into dotted columns (account.username, address.city, ...). Any callable (std::uint64_t index, user&) works.| Exception | When |
|---|---|
std::runtime_error | Any generation call before the account tables or a library has data; a user with a device when device-generator has none |
std::invalid_argument | ages.min > ages.max, or ages.min < sign_up_age; reference_date or service_start is not a date; a password policy with min_length of 0, min_length > max_length, max_length over 256, no allowed class, or require_each with fewer characters than classes; an unknown status; a country that is not a code or that a library has no data for (including a name culture or birth table without the sex asked for, and device-generator for a user with a device); a user with a device whose platform device-generator does not know (from a layer of platforms.tsv); extend of a file that is not one of the user tables |
dasmig::core::tsv_error | A malformed table or layer (message gives file, line and column); the data is unchanged |