Pipeline: City rename, add address/postal-code generation, add xeger generation (#232)

This commit is contained in:
Aaron Po
2026-07-19 03:59:06 -04:00
parent 901f30ffd8
commit 8e7ecf2fc3
56 changed files with 2454 additions and 1151 deletions

View File

@@ -58,6 +58,12 @@
#include "services/enrichment/mock_enrichment.h"
#include "services/enrichment/wikipedia_service.h"
// --- services: postal_code ---
#include "services/postal_code/mock_postal_code_service.h"
#include "services/postal_code/postal_code_service.h"
#include "services/postal_code/xeger_postal_code_service.h"
#include "services/xeger/xeger.h"
// --- services: logging ---
#include "services/logging/log_dispatcher.h"
#include "services/logging/log_entry.h"

View File

@@ -16,6 +16,7 @@
#include "services/database/export_service.h"
#include "services/enrichment/enrichment_service.h"
#include "services/logging/logger.h"
#include "services/postal_code/postal_code_service.h"
/**
* @brief Main orchestrator for the Biergarten data generation pipeline.
@@ -34,6 +35,8 @@ class BiergartenPipelineOrchestrator {
* @param exporter Database backend for persisting generated records.
* @param curated_data_service Loads curated location, persona, and name
* data used to seed generation.
* @param postal_code_service Resolves a postal code for a city from its
* curated postal-code data.
* @param application_options CLI configuration and paths.
*/
BiergartenPipelineOrchestrator(
@@ -42,6 +45,7 @@ class BiergartenPipelineOrchestrator {
std::unique_ptr<DataGenerator> generator,
std::unique_ptr<IExportService> exporter,
std::unique_ptr<ICuratedDataService> curated_data_service,
std::unique_ptr<IPostalCodeService> postal_code_service,
const ApplicationOptions& application_options);
/**
@@ -78,6 +82,7 @@ class BiergartenPipelineOrchestrator {
*/
std::unique_ptr<IExportService> exporter_;
std::unique_ptr<ICuratedDataService> curated_data_service_;
std::unique_ptr<IPostalCodeService> postal_code_service_;
ApplicationOptions application_options_;
@@ -87,7 +92,7 @@ class BiergartenPipelineOrchestrator {
* @return Vector of locations randomly sampled per
* PipelineOptions::location_count.
*/
std::vector<Location> QueryCitiesWithCountries();
std::vector<City> QueryLocations();
/**
* @brief Generate breweries for enriched cities.

View File

@@ -26,9 +26,7 @@
*/
template <typename T>
class BoundedChannel {
// -------------------------------------------------------------------------
// Internal state — all access must be guarded by mutex_.
// -------------------------------------------------------------------------
std::queue<T> queue_;

View File

@@ -20,12 +20,10 @@ class DataGenerator {
/**
* @brief Generates brewery data for a location.
*
* @param location Location data
* @param region_context Additional regional context text.
* @param city Enriched city the brewery is associated with.
* @return Brewery generation result.
*/
virtual BreweryResult GenerateBrewery(const Location& location,
const std::string& region_context) = 0;
virtual BreweryResult GenerateBrewery(const EnrichedCity& city) = 0;
/**
* @brief Generates a user profile grounded in a sampled name and persona.

View File

@@ -51,12 +51,11 @@ class LlamaGenerator final : public DataGenerator {
/**
* @brief Generates brewery data for a specific location.
*
* @param location Location object.
* @param location City object.
* @param region_context Additional regional context.
* @return Generated brewery result.
*/
BreweryResult GenerateBrewery(const Location& location,
const std::string& region_context) override;
BreweryResult GenerateBrewery(const EnrichedCity&) override;
/**
* @brief Generates a user profile grounded in a sampled name and persona.

View File

@@ -20,20 +20,15 @@ class MockGenerator final : public DataGenerator {
/**
* @brief Generates deterministic brewery data for a location.
*
* @param location City and country names.
* @param region_context Unused for mock generation.
* @param enriched_city
* @return Generated brewery result.
*/
BreweryResult GenerateBrewery(const Location& location,
const std::string& region_context) override;
BreweryResult GenerateBrewery(const EnrichedCity& enriched_city) override;
/**
* @brief Generates deterministic user data grounded in a sampled name and
* persona.
*
* @param city Enriched city the user is associated with.
* @param persona Persona archetype.
* @param name Sampled first/last name, copied directly into the result.
* @return Generated user result.
*/
UserResult GenerateUser(const EnrichedCity& city, const UserPersona& persona,
@@ -46,7 +41,7 @@ class MockGenerator final : public DataGenerator {
* @param location City and country names.
* @return Deterministic hash value.
*/
static size_t DeterministicHash(const Location& location);
static size_t DeterministicHash(const City& location);
/**
* @brief Combines city, persona, and name into a stable hash value.
@@ -56,7 +51,7 @@ class MockGenerator final : public DataGenerator {
* @param name Sampled first/last name.
* @return Deterministic hash value.
*/
static size_t DeterministicHash(const Location& location,
static size_t DeterministicHash(const City& location,
const UserPersona& persona, const Name& name);
// Hash stride constants for deterministic distribution across fixed-size

View File

@@ -11,9 +11,7 @@
#include "data_model/models.h"
// ============================================================================
// Generation Output Models
// ============================================================================
/**
* @brief Generated brewery payload.
@@ -79,23 +77,39 @@ struct UserResult {
float activity_weight{};
};
// ============================================================================
// Pipeline Data Models
// ============================================================================
/**
* @brief Enriched city data with Wikipedia context.
*/
struct EnrichedCity {
Location location;
City location;
std::string region_context{};
};
/**
* @brief A physical address: the city plus a concrete postal code selected
* from that city's curated postal-code examples.
*/
struct BreweryAddress {
City city;
std::string postal_code{};
};
/**
* @brief A physical address for a generated user: the city plus a concrete
* postal code selected from that city's curated postal-code examples.
*/
struct UserAddress {
City city;
std::string postal_code{};
};
/**
* @brief Helper struct to store generated brewery data.
*/
struct BreweryRecord {
Location location;
BreweryAddress address;
BreweryResult brewery;
};
@@ -107,7 +121,7 @@ struct BreweryRecord {
* consumer can register real accounts from the pipeline's SQLite export.
*/
struct UserRecord {
Location location;
UserAddress address;
UserResult user;
std::string email{};
std::string date_of_birth{};

View File

@@ -21,14 +21,36 @@ class ILogger;
namespace prog_opts = boost::program_options;
// ============================================================================
// Location Models
// ============================================================================
// City Models
/**
* @brief Canonical location record for city-level generation.
* @brief Curated postal-code metadata for a city, mirroring the
* `postal_code` object in the locations dataset.
*/
struct Location {
struct PostalCodeSpec {
/**
* @brief Permissive country-wide postal-code format regex, used as the
* validation fallback for a selected example.
*/
std::string country_format_regex{};
/**
* @brief City-specific postal-code regexes (more precise than the country
* format).
*/
std::vector<std::string> city_regexes{};
/**
* @brief Concrete example postal codes for this city. The postal code
* service selects from these rather than synthesizing codes.
*/
std::vector<std::string> examples{};
};
/**
* @brief Canonical city record for city-level generation.
*/
struct City {
std::string city{};
std::string state_province{};
@@ -50,19 +72,13 @@ struct Location {
std::vector<std::string> local_languages{};
/**
* @brief Latitude in decimal degrees.
* @brief Postal-code metadata (format regex, city regexes, and concrete
* examples) sourced directly from the curated locations dataset.
*/
double latitude{};
/**
* @brief Longitude in decimal degrees.
*/
double longitude{};
PostalCodeSpec postal_code{};
};
// ============================================================================
// Name / Persona Models
// ============================================================================
/**
* @brief A sampled first/last name pair, with the source forename's gender.
@@ -131,9 +147,7 @@ struct UserPersona {
std::vector<std::string> style_affinities{};
};
// ============================================================================
// Configuration Models
// ============================================================================
/**
* @brief LLM sampling parameters.
@@ -223,9 +237,7 @@ struct ApplicationOptions {
PipelineOptions pipeline;
};
// ============================================================================
// Function Declarations
// ============================================================================
std::optional<ApplicationOptions> ParseArguments(
const int argc, char** argv, std::shared_ptr<ILogger> logger = nullptr);

View File

@@ -15,7 +15,7 @@
using ForenameList = std::vector<ForenameEntry>;
using SurnameList = std::vector<std::string>;
using LocationsList = std::vector<Location>;
using LocationsList = std::vector<City>;
using PersonasList = std::vector<UserPersona>;
using ForenamesByCountryMap = std::unordered_map<std::string, ForenameList>;
using SurnamesByCountryMap = std::unordered_map<std::string, SurnameList>;

View File

@@ -45,27 +45,29 @@ class SqliteExportService final : public IExportService {
void RollbackAndCloseNoThrow() noexcept;
[[nodiscard]] std::filesystem::path BuildDatabasePath() const;
[[nodiscard]] static std::string BuildLocationKey(const Location& location);
[[nodiscard]] static std::string BuildCityKey(const City& city);
/**
* @brief Returns the row id for @p location, inserting it first if it has
* @brief Returns the row id for @p city, inserting it first if it has
* not already been seen during this run.
*
* Shared by both ProcessRecord() overloads so breweries and users
* referencing the same location resolve to the same row.
* Shared by both ProcessRecord() overloads so brewery and user addresses
* referencing the same city resolve to the same row.
*/
[[nodiscard]] sqlite3_int64 ResolveLocationId(const Location& location);
[[nodiscard]] sqlite3_int64 ResolveCityId(const City& city);
std::unique_ptr<IDateTimeProvider> date_time_provider_;
std::filesystem::path output_path_;
std::string run_timestamp_utc_;
std::filesystem::path database_path_;
SqliteDatabaseHandle db_handle_;
SqliteStatementHandle insert_location_stmt_;
SqliteStatementHandle insert_city_stmt_;
SqliteStatementHandle insert_brewery_stmt_;
SqliteStatementHandle insert_brewery_address_stmt_;
SqliteStatementHandle insert_user_stmt_;
SqliteStatementHandle insert_user_address_stmt_;
bool transaction_open_ = false;
std::unordered_map<std::string, sqlite3_int64> location_cache_;
std::unordered_map<std::string, sqlite3_int64> city_cache_;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_H_

View File

@@ -17,9 +17,9 @@
namespace sqlite_export_service_internal {
inline constexpr std::string_view kCreateLocationsTableSql = R"sql(
inline constexpr std::string_view kCreateCitiesTableSql = R"sql(
CREATE TABLE IF NOT EXISTS locations (
CREATE TABLE IF NOT EXISTS cities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
city TEXT NOT NULL,
state_province TEXT NOT NULL,
@@ -27,9 +27,9 @@ CREATE TABLE IF NOT EXISTS locations (
country TEXT NOT NULL,
iso3166_1 TEXT NOT NULL,
local_languages_json TEXT NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
UNIQUE(city, state_province, iso3166_2, country, latitude, longitude)
postal_code_country_format_regex TEXT NOT NULL,
postal_code_city_regex_json TEXT NOT NULL,
UNIQUE(city, state_province, iso3166_2, country)
);
)sql";
@@ -38,15 +38,29 @@ inline constexpr std::string_view kCreateBreweriesTableSql = R"sql(
CREATE TABLE IF NOT EXISTS breweries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
name_en TEXT NOT NULL,
description_en TEXT NOT NULL,
name_local TEXT NOT NULL,
description_local TEXT NOT NULL,
FOREIGN KEY(location_id) REFERENCES locations(id) ON DELETE CASCADE
description_local TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_breweries_location_id ON breweries(location_id);
)sql";
inline constexpr std::string_view kCreateBreweryAddressesTableSql = R"sql(
CREATE TABLE IF NOT EXISTS brewery_addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
brewery_id INTEGER NOT NULL,
city_id INTEGER NOT NULL,
postal_code TEXT NOT NULL,
FOREIGN KEY(brewery_id) REFERENCES breweries(id) ON DELETE CASCADE,
FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_brewery_addresses_brewery_id
ON brewery_addresses(brewery_id);
CREATE INDEX IF NOT EXISTS idx_brewery_addresses_city_id
ON brewery_addresses(city_id);
)sql";
@@ -54,7 +68,6 @@ inline constexpr std::string_view kCreateUsersTableSql = R"sql(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
gender TEXT NOT NULL,
@@ -62,40 +75,61 @@ CREATE TABLE IF NOT EXISTS users (
bio TEXT NOT NULL,
activity_weight REAL NOT NULL,
email TEXT NOT NULL UNIQUE,
date_of_birth TEXT NOT NULL,
FOREIGN KEY(location_id) REFERENCES locations(id) ON DELETE CASCADE
date_of_birth TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_users_location_id ON users(location_id);
)sql";
inline constexpr std::string_view kInsertLocationSql = R"sql(
INSERT INTO locations (
inline constexpr std::string_view kCreateUserAddressesTableSql = R"sql(
CREATE TABLE IF NOT EXISTS user_addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
city_id INTEGER NOT NULL,
postal_code TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_user_addresses_user_id
ON user_addresses(user_id);
CREATE INDEX IF NOT EXISTS idx_user_addresses_city_id
ON user_addresses(city_id);
)sql";
inline constexpr std::string_view kInsertCitySql = R"sql(
INSERT INTO cities (
city,
state_province,
iso3166_2,
country,
iso3166_1,
local_languages_json,
latitude,
longitude
postal_code_country_format_regex,
postal_code_city_regex_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?);
)sql";
inline constexpr std::string_view kInsertBrewerySql = R"sql(
INSERT INTO breweries (
location_id,
name_en,
description_en,
name_local,
description_local
) VALUES (?, ?, ?, ?, ?);
) VALUES (?, ?, ?, ?);
)sql";
inline constexpr std::string_view kInsertBreweryAddressSql = R"sql(
INSERT INTO brewery_addresses (
brewery_id,
city_id,
postal_code
) VALUES (?, ?, ?);
)sql";
inline constexpr std::string_view kInsertUserSql = R"sql(
INSERT INTO users (
location_id,
first_name,
last_name,
gender,
@@ -104,33 +138,45 @@ INSERT INTO users (
activity_weight,
email,
date_of_birth
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
) VALUES (?, ?, ?, ?, ?, ?, ?, ?);
)sql";
inline constexpr std::string_view kInsertUserAddressSql = R"sql(
INSERT INTO user_addresses (
user_id,
city_id,
postal_code
) VALUES (?, ?, ?);
)sql";
// sqlite3_bind_*() parameter indices are 1-based, matching the "?"
// placeholder order in the SQL above.
enum LocationBindIndex {
kLocationCityBindIndex = 1,
kLocationStateProvinceBindIndex,
kLocationIso31662BindIndex,
kLocationCountryBindIndex,
kLocationIso31661BindIndex,
kLocationLanguagesBindIndex,
kLocationLatitudeBindIndex,
kLocationLongitudeBindIndex,
enum CityBindIndex {
kCityCityBindIndex = 1,
kCityStateProvinceBindIndex,
kCityIso31662BindIndex,
kCityCountryBindIndex,
kCityIso31661BindIndex,
kCityLanguagesBindIndex,
kCityPostalCodeCountryFormatRegexBindIndex,
kCityPostalCodeCityRegexJsonBindIndex,
};
enum BreweryBindIndex {
kBreweryLocationIdBindIndex = 1,
kBreweryEnglishNameBindIndex,
kBreweryEnglishNameBindIndex = 1,
kBreweryEnglishDescriptionBindIndex,
kBreweryLocalNameBindIndex,
kBreweryLocalDescriptionBindIndex,
};
enum BreweryAddressBindIndex {
kBreweryAddressBreweryIdBindIndex = 1,
kBreweryAddressCityIdBindIndex,
kBreweryAddressPostalCodeBindIndex,
};
enum UserBindIndex {
kUserLocationIdBindIndex = 1,
kUserFirstNameBindIndex,
kUserFirstNameBindIndex = 1,
kUserLastNameBindIndex,
kUserGenderBindIndex,
kUserUsernameBindIndex,
@@ -140,6 +186,12 @@ enum UserBindIndex {
kUserDateOfBirthBindIndex,
};
enum UserAddressBindIndex {
kUserAddressUserIdBindIndex = 1,
kUserAddressCityIdBindIndex,
kUserAddressPostalCodeBindIndex,
};
SqliteStatementHandle PrepareStatement(const SqliteDatabaseHandle& db_handle,
std::string_view sql,
const char* action);

View File

@@ -15,6 +15,9 @@
/**
* @brief Interface for UTC timestamp providers.
*
* @todo Yagni, consider removing this and just use SystemDateTimeProvider
* directly
*/
class IDateTimeProvider {
public:

View File

@@ -20,10 +20,10 @@ class IEnrichmentService {
/**
* @brief Resolves contextual enrichment for a location.
*
* @param loc Location to enrich.
* @param loc City to enrich.
* @return Context text, or an empty string if unavailable.
*/
virtual std::string GetLocationContext(const Location& loc) = 0;
virtual std::string GetLocationContext(const City& loc) = 0;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_ENRICHMENT_SERVICE_H_

View File

@@ -15,7 +15,7 @@
*/
class MockEnrichmentService final : public IEnrichmentService {
public:
std::string GetLocationContext(const Location& /*loc*/) override {
std::string GetLocationContext(const City& /*loc*/) override {
return {};
}
};

View File

@@ -29,7 +29,7 @@ class WikipediaEnrichmentService final : public IEnrichmentService {
/**
* @brief Returns the Wikipedia-derived context for a location.
*/
[[nodiscard]] std::string GetLocationContext(const Location& loc) override;
[[nodiscard]] std::string GetLocationContext(const City& loc) override;
private:
std::string FetchExtract(std::string_view query);

View File

@@ -51,7 +51,7 @@ enum class PipelinePhase {
*/
Startup,
/**
* @brief Location/context enrichment (e.g. Wikipedia lookups).
* @brief City/context enrichment (e.g. Wikipedia lookups).
*/
Enrichment,
/**

View File

@@ -0,0 +1,79 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_MOCK_POSTAL_CODE_SERVICE_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_MOCK_POSTAL_CODE_SERVICE_H_
/**
* @file services/postal_code/mock_postal_code_service.h
* @brief Data-driven postal code service that selects the first curated
* example for a city and validates it against the city's postal-code regex.
*/
#include <format>
#include <regex>
#include <stdexcept>
#include <string>
#include "services/postal_code/postal_code_service.h"
/**
* @brief Postal code service backed entirely by the curated locations
* dataset: it returns the city's first example postal code after checking it
* against the curated postal-code regex.
*
* A precise city regex is preferred when present; otherwise the permissive
* country format regex is used as the fallback. A city with no examples, or an
* example that fails validation, throws so the caller can skip the city.
*/
class MockPostalCodeService final : public IPostalCodeService {
public:
std::string GeneratePostalCode(const City& city) override {
const PostalCodeSpec& spec = city.postal_code;
if (spec.examples.empty()) {
throw std::runtime_error(std::format(
"No postal-code examples available for city '{}'", city.city));
}
const std::string& candidate = spec.examples.front();
if (!MatchesAnyPattern(candidate, spec)) {
throw std::runtime_error(
std::format("Postal code '{}' for city '{}' does not match the "
"curated postal-code regex",
candidate, city.city));
}
return candidate;
}
private:
/**
* @brief Returns true if @p candidate matches any city regex, or the
* country format regex when no city regex matches. When the spec carries no
* usable regex at all, validation is skipped (returns true).
*/
static bool MatchesAnyPattern(const std::string& candidate,
const PostalCodeSpec& spec) {
bool has_pattern = false;
for (const std::string& pattern : spec.city_regexes) {
if (pattern.empty()) {
continue;
}
has_pattern = true;
if (std::regex_match(candidate, std::regex(pattern))) {
return true;
}
}
if (!spec.country_format_regex.empty()) {
has_pattern = true;
if (std::regex_match(candidate, std::regex(spec.country_format_regex))) {
return true;
}
}
return !has_pattern;
}
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_MOCK_POSTAL_CODE_SERVICE_H_

View File

@@ -0,0 +1,35 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_POSTAL_CODE_SERVICE_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_POSTAL_CODE_SERVICE_H_
/**
* @file services/postal_code/postal_code_service.h
* @brief Abstraction for resolving a postal code for a city.
*/
#include <string>
#include "data_model/models.h"
/**
* @brief Interface for services that resolve a postal code for a city.
*/
class IPostalCodeService {
public:
IPostalCodeService() = default;
virtual ~IPostalCodeService() = default;
IPostalCodeService(const IPostalCodeService&) = delete;
IPostalCodeService& operator=(const IPostalCodeService&) = delete;
IPostalCodeService(IPostalCodeService&&) = delete;
IPostalCodeService& operator=(IPostalCodeService&&) = delete;
/**
* @brief Resolves a postal code for @p city.
*
* @param city City whose curated postal-code examples supply the result.
* @return A postal code string, or an empty string if none is available.
*/
virtual std::string GeneratePostalCode(const City& city) = 0;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_POSTAL_CODE_SERVICE_H_

View File

@@ -0,0 +1,32 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_XEGER_POSTAL_CODE_SERVICE_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_XEGER_POSTAL_CODE_SERVICE_H_
/**
* @file services/postal_code/xeger_postal_code_service.h
* @brief Postal code service that generates a fresh, format-conformant code
* per call from the curated regex patterns, instead of returning a fixed
* example.
*/
#include <random>
#include "services/postal_code/postal_code_service.h"
/**
* @brief Postal code service using regex-driven generation (xeger).
*
* Picks among the city's curated `city_regexes` (falls back to
* `country_format_regex`), then generates a random string matching that pattern
* via GenerateStringFromRegex().
*/
class XegerPostalCodeService final : public IPostalCodeService {
public:
XegerPostalCodeService();
std::string GeneratePostalCode(const City& city) override;
private:
std::mt19937 rng_;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_XEGER_POSTAL_CODE_SERVICE_H_

View File

@@ -0,0 +1,38 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_XEGER_XEGER_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_XEGER_XEGER_H_
#include <random>
#include <string>
#include <string_view>
/**
* @brief Generates a random string matching @p pattern.
*
* Supported syntax:
* - literals
* - `.`
* - alternation `|`
* - groups `(...)` and non-capturing groups `(?:...)`
* - quantifiers `*` `+` `?` `{n}` `{n,}` `{n,m}`
* - character classes `[...]` / `[^...]`
* - shorthand escapes `\d \D \w \W \s \S \n \t \r`
*
* Not supported: lookaround, backreferences, and lazy quantifiers.
*
* @param pattern Regular expression to generate a matching
* string for.
* @param rng Random source driving alternation/quantifier/
* class choices.
* @param unbounded_repeat_cap Extra repeats allowed on top of the declared
* minimum for unbounded quantifiers
* (`*`, `+`, `{n,}`).
*
* @return A string satisfying `std::regex_match(result, std::regex(pattern))`
* for any @p pattern within the supported syntax subset.
*
* @throws std::runtime_error if @p pattern cannot be parsed.
*/
std::string GenerateStringFromRegex(std::string_view pattern, std::mt19937& rng,
int unbounded_repeat_cap = 8);
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_XEGER_XEGER_H_

View File

@@ -0,0 +1,201 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_XEGER_XEGER_INTERNAL_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_XEGER_XEGER_INTERNAL_H_
/**
* @file services/xeger/xeger_internal.h
* @brief Declarations shared by the xeger parser and generator.
*
* 1. Parser Turns the pattern text into an AST (Literal, AnyChar,
* CharClass, Concat, Alternation, Repeat nodes).
* 2. Visitor Double-dispatch contract -- each AST-walking
* operation implements this.
* 3. XegerGenerator a Visitor that emits a random character wherever the
* AST says a character belongs, respecting alternation
* choices and repeat counts.
*/
#include <memory>
#include <optional>
#include <random>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xeger_internal {
// Regex syntax characters
constexpr char kAnchorStart = '^';
constexpr char kAnchorEnd = '$';
constexpr char kAlternation = '|';
constexpr char kGroupOpen = '(';
constexpr char kGroupClose = ')';
constexpr char kNonCapturingMarker = '?';
constexpr char kNonCapturingColon = ':';
constexpr char kClassOpen = '[';
constexpr char kClassClose = ']';
constexpr char kClassNegate = '^';
constexpr char kClassRange = '-';
constexpr char kAnyChar = '.';
constexpr char kEscape = '\\';
constexpr char kStar = '*';
constexpr char kPlus = '+';
constexpr char kOptional = '?';
constexpr char kBraceOpen = '{';
constexpr char kBraceClose = '}';
constexpr char kBraceComma = ',';
// AST
struct Literal;
struct AnyChar;
struct CharClass;
struct Concat;
struct Alternation;
struct Repeat;
struct Visitor {
virtual void Visit(const Literal&) = 0;
virtual void Visit(const AnyChar&) = 0;
virtual void Visit(const CharClass&) = 0;
virtual void Visit(const Concat&) = 0;
virtual void Visit(const Alternation&) = 0;
virtual void Visit(const Repeat&) = 0;
virtual ~Visitor() = default;
};
struct Node {
virtual void Accept(Visitor&) const = 0;
virtual ~Node() = default;
};
using NodePtr = std::unique_ptr<Node>;
template <typename Derived>
struct Visitable : Node {
void Accept(Visitor& visitor) const override {
visitor.Visit(static_cast<const Derived&>(*this));
}
};
// One fixed character, e.g. 'a'.
struct Literal : Visitable<Literal> {
char ch;
explicit Literal(const char c) : ch(c) {}
};
// The "." wildcard: any single printable character.
struct AnyChar : Visitable<AnyChar> {};
// A set of allowed characters, stored as inclusive ranges. A single
// character 'x' is the range {'x', 'x'}. `negated` flips the meaning to
// "any character NOT listed" (i.e. `[^...]`).
struct CharClass : Visitable<CharClass> {
std::vector<std::pair<char, char>> ranges;
bool negated = false;
};
// "Do these children one after another."
struct Concat : Visitable<Concat> {
std::vector<NodePtr> parts;
};
// "Pick exactly one of these options at random" -- the `|` operator.
struct Alternation : Visitable<Alternation> {
std::vector<NodePtr> options;
};
// "Repeat one child between min and max times."
// a* -> min 0, max infinite
// a+ -> min 1, max infinite
// a? -> min 0, max 1
// a{2,5} -> min 2, max 5
struct Repeat : Visitable<Repeat> {
NodePtr child;
unsigned int min = 0;
std::optional<unsigned int> max; // std::nullopt means "no upper bound"
};
// Parser (recursive descent)
//
// Precedence, loosest to tightest binding:
// ParseAlternation ::= a|b|c
// ParseConcat ::= abc
// ParseRepeat ::= a* a{2,5}
// ParseAtom ::= a . [..] (..) \d
class Parser {
public:
explicit Parser(std::string_view pattern);
NodePtr Parse();
private:
std::string src_;
size_t pos_ = 0;
// Strips anchors (leading '^' and trailing '$') as they are not needed for
// generation.
static std::string StripAnchors(std::string_view pattern);
[[nodiscard]] bool AtEnd() const;
[[nodiscard]] char Peek() const;
char Advance();
bool Match(char c);
void Expect(char c);
// Level 1: alternation -- ( concat | concat | concat )
NodePtr ParseAlternation();
// Level 2: concatenation -- a run of items back to back
NodePtr ParseConcat();
// Level 3: an atom plus an optional quantifier
NodePtr ParseRepeat();
// Curly-brace quantifiers: {n}, {n,}, {n,m}
NodePtr ParseBrace(NodePtr atom);
// Level 4: an atom -- the smallest standalone unit
NodePtr ParseAtom();
static NodePtr MakeClass(std::vector<std::pair<char, char>> ranges,
bool negated);
NodePtr ParseEscape();
NodePtr ParseCharClass();
// Reads one character inside [ ], turning \] \n etc. into the real
// character they represent.
char ClassChar();
};
// Generator -- a Visitor that emits a random matching string
class XegerGenerator final : public Visitor {
public:
XegerGenerator(std::mt19937& rng, int unbounded_cap);
std::string Run(const Node& root);
void Visit(const Literal& node) override;
void Visit(const AnyChar&) override;
void Visit(const CharClass& node) override;
void Visit(const Concat& node) override;
void Visit(const Alternation& node) override;
void Visit(const Repeat& node) override;
private:
std::mt19937& rng_;
int unbounded_cap_;
std::string out_;
static bool InRanges(const CharClass& node, char c);
char PickFromClass(const CharClass& node);
};
} // namespace xeger_internal
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_XEGER_XEGER_INTERNAL_H_