Pipeline: rename Location to City, wire postal code into breweries

This commit is contained in:
Aaron Po
2026-07-13 02:26:44 -04:00
parent fbcf438381
commit d52dba904c
48 changed files with 2261 additions and 1227 deletions

View File

@@ -140,6 +140,8 @@ FetchContent_MakeAvailable(cpp-httplib)
add_executable(${PROJECT_NAME}
includes/services/enrichment/mock_enrichment.h
includes/services/curated_data/mock_curated_data_service.h
includes/services/postal_code/postal_code_service.h
includes/services/postal_code/mock_postal_code_service.h
includes/json_handling/pretty_print.h)
# --- Entry point ---
@@ -264,8 +266,8 @@ target_compile_options(biergarten-pipeline PRIVATE
# 7. Runtime Assets
configure_file(
${CMAKE_SOURCE_DIR}/locations.json
${CMAKE_BINARY_DIR}/locations.json
${CMAKE_SOURCE_DIR}/cities.json
${CMAKE_BINARY_DIR}/cities.json
COPYONLY
)
configure_file(
@@ -288,4 +290,3 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
${CMAKE_SOURCE_DIR}/prompts
${CMAKE_BINARY_DIR}/prompts
)

1718
tooling/pipeline/cities.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,10 @@
#include "services/logging/log_producer.h"
#include "services/logging/logger.h"
// --- services: postal_code ---
#include "services/postal_code/mock_postal_code_service.h"
#include "services/postal_code/postal_code_service.h"
// --- services: prompting ---
#include "services/prompting/prompt_directory.h"

View File

@@ -16,7 +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 +34,7 @@ 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
* @param application_options CLI configuration and paths.
*/
BiergartenPipelineOrchestrator(
@@ -42,6 +43,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 +80,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 +90,7 @@ class BiergartenPipelineOrchestrator {
* @return Vector of locations randomly sampled per
* PipelineOptions::location_count.
*/
std::vector<Location> QueryCitiesWithCountries();
std::vector<City> QueryCitiesWithCountries();
/**
* @brief Generate breweries for enriched cities.

View File

@@ -24,7 +24,7 @@ class DataGenerator {
* @param region_context Additional regional context text.
* @return Brewery generation result.
*/
virtual BreweryResult GenerateBrewery(const Location& location,
virtual BreweryResult GenerateBrewery(const City& location,
const std::string& region_context) = 0;
/**

View File

@@ -55,7 +55,7 @@ class LlamaGenerator final : public DataGenerator {
* @param region_context Additional regional context.
* @return Generated brewery result.
*/
BreweryResult GenerateBrewery(const Location& location,
BreweryResult GenerateBrewery(const City& location,
const std::string& region_context) override;
/**

View File

@@ -24,7 +24,7 @@ class MockGenerator final : public DataGenerator {
* @param region_context Unused for mock generation.
* @return Generated brewery result.
*/
BreweryResult GenerateBrewery(const Location& location,
BreweryResult GenerateBrewery(const City& location,
const std::string& region_context) override;
/**
@@ -46,7 +46,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 +56,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

@@ -87,15 +87,27 @@ struct UserResult {
* @brief Enriched city data with Wikipedia context.
*/
struct EnrichedCity {
Location location;
City location;
std::string region_context{};
};
/**
* @brief A brewery's street-level address, distinct from the shared `City`
* it belongs to. Mirrors the backend's `BreweryPostLocation` shape.
*
* Only `postal_code` is populated today -- street-address generation has no
* fixture data or service yet.
*/
struct Address {
std::string postal_code{};
};
/**
* @brief Helper struct to store generated brewery data.
*/
struct BreweryRecord {
Location location;
City location;
Address address;
BreweryResult brewery;
};
@@ -107,7 +119,7 @@ struct BreweryRecord {
* consumer can register real accounts from the pipeline's SQLite export.
*/
struct UserRecord {
Location location;
City location;
UserResult user;
std::string email{};
std::string date_of_birth{};

View File

@@ -22,13 +22,13 @@ class ILogger;
namespace prog_opts = boost::program_options;
// ============================================================================
// Location Models
// City Models
// ============================================================================
/**
* @brief Canonical location record for city-level generation.
*/
struct Location {
struct City {
std::string city{};
std::string state_province{};
@@ -44,6 +44,18 @@ struct Location {
*/
std::string iso3166_1{};
/**
* @brief A list of regular expressions used for valid postal codes in the
* region.
*
*/
std::vector<std::string> postal_regex{};
/**
* @brief Example postal codes for the region.
*/
std::vector<std::string> postal_code_examples{};
/**
* @brief Local language codes in priority order.
*/

View File

@@ -15,7 +15,7 @@
using ForenameList = std::vector<ForenameEntry>;
using SurnameList = std::vector<std::string>;
using LocationsList = std::vector<Location>;
using CityList = std::vector<City>;
using PersonasList = std::vector<UserPersona>;
using ForenamesByCountryMap = std::unordered_map<std::string, ForenameList>;
using SurnamesByCountryMap = std::unordered_map<std::string, SurnameList>;
@@ -37,7 +37,7 @@ class ICuratedDataService {
/**
* @brief Loads all curated location records.
*/
virtual const LocationsList& LoadLocations() = 0;
virtual const CityList& LoadCities() = 0;
/**
* @brief Loads all curated persona records.

View File

@@ -16,7 +16,7 @@
* CuratedJsonDataService.
*/
struct CuratedDataFilePaths {
std::filesystem::path locations_path;
std::filesystem::path cities_path;
std::filesystem::path personas_path;
std::filesystem::path forenames_path;
std::filesystem::path surnames_path;
@@ -27,7 +27,7 @@ struct CuratedDataFilePaths {
*/
class CuratedJsonDataService final : public ICuratedDataService {
struct cache {
LocationsList locations;
CityList locations;
PersonasList personas;
ForenamesByCountryMap forenames_by_country;
SurnamesByCountryMap surnames_by_country;
@@ -43,9 +43,9 @@ class CuratedJsonDataService final : public ICuratedDataService {
explicit CuratedJsonDataService(CuratedDataFilePaths filepaths);
/**
* @brief Parses a JSON array file and returns all location records.
* @brief Parses a JSON array file and returns all city records.
*/
const LocationsList& LoadLocations() override;
const CityList& LoadCities() override;
/**
* @brief Parses a JSON array file and returns all persona records.

View File

@@ -20,7 +20,7 @@ class MockCuratedDataService final : public ICuratedDataService {
public:
MockCuratedDataService();
const LocationsList& LoadLocations() override;
const CityList& LoadCities() override;
const PersonasList& LoadPersonas() override;
@@ -29,7 +29,7 @@ class MockCuratedDataService final : public ICuratedDataService {
const SurnamesByCountryMap& LoadSurnamesByCountry() override;
private:
LocationsList locations_;
CityList locations_;
PersonasList personas_;
ForenamesByCountryMap forenames_by_country_;
SurnamesByCountryMap surnames_by_country_;

View File

@@ -45,7 +45,7 @@ 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& location);
/**
* @brief Returns the row id for @p location, inserting it first if it has
@@ -54,18 +54,18 @@ class SqliteExportService final : public IExportService {
* Shared by both ProcessRecord() overloads so breweries and users
* referencing the same location resolve to the same row.
*/
[[nodiscard]] sqlite3_int64 ResolveLocationId(const Location& location);
[[nodiscard]] sqlite3_int64 ResolveCityId(const City& location);
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_user_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,
@@ -38,15 +38,16 @@ inline constexpr std::string_view kCreateBreweriesTableSql = R"sql(
CREATE TABLE IF NOT EXISTS breweries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
city_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
postal_code TEXT NOT NULL,
FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_breweries_location_id ON breweries(location_id);
CREATE INDEX IF NOT EXISTS idx_breweries_city_id ON breweries(city_id);
)sql";
@@ -54,7 +55,7 @@ inline constexpr std::string_view kCreateUsersTableSql = R"sql(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
city_id INTEGER NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
gender TEXT NOT NULL,
@@ -63,15 +64,15 @@ CREATE TABLE IF NOT EXISTS users (
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
FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_users_location_id ON users(location_id);
CREATE INDEX IF NOT EXISTS idx_users_city_id ON users(city_id);
)sql";
inline constexpr std::string_view kInsertLocationSql = R"sql(
INSERT INTO locations (
inline constexpr std::string_view kInsertCitySql = R"sql(
INSERT INTO cities (
city,
state_province,
iso3166_2,
@@ -85,17 +86,18 @@ INSERT INTO locations (
inline constexpr std::string_view kInsertBrewerySql = R"sql(
INSERT INTO breweries (
location_id,
city_id,
name_en,
description_en,
name_local,
description_local
) VALUES (?, ?, ?, ?, ?);
description_local,
postal_code
) VALUES (?, ?, ?, ?, ?, ?);
)sql";
inline constexpr std::string_view kInsertUserSql = R"sql(
INSERT INTO users (
location_id,
city_id,
first_name,
last_name,
gender,
@@ -109,27 +111,28 @@ INSERT INTO users (
// 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 {
kCityNameBindIndex = 1,
kCityStateProvinceBindIndex,
kCityIso31662BindIndex,
kCityCountryBindIndex,
kCityIso31661BindIndex,
kCityLanguagesBindIndex,
kCityLatitudeBindIndex,
kCityLongitudeBindIndex,
};
enum BreweryBindIndex {
kBreweryLocationIdBindIndex = 1,
kBreweryCityIdBindIndex = 1,
kBreweryEnglishNameBindIndex,
kBreweryEnglishDescriptionBindIndex,
kBreweryLocalNameBindIndex,
kBreweryLocalDescriptionBindIndex,
kBreweryPostalCodeBindIndex,
};
enum UserBindIndex {
kUserLocationIdBindIndex = 1,
kUserCityIdBindIndex = 1,
kUserFirstNameBindIndex,
kUserLastNameBindIndex,
kUserGenderBindIndex,

View File

@@ -23,7 +23,7 @@ class IEnrichmentService {
* @param loc Location 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

@@ -0,0 +1,32 @@
#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 Placeholder IPostalCodeService used until xeger-based generation is
* implemented.
*/
#include <stdexcept>
#include <string>
#include "data_model/models.h"
#include "services/postal_code/postal_code_service.h"
/**
* @brief Postal code service that returns the location's first known
* example postal code rather than generating one from its regex.
*/
class MockPostalCodeService final : public IPostalCodeService {
public:
std::string GeneratePostalCode(const City& location) override {
if (location.postal_code_examples.empty()) {
throw std::runtime_error(
"MockPostalCodeService: location has no postal_code_examples: " +
location.city);
}
return location.postal_code_examples.front();
}
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_MOCK_POSTAL_CODE_SERVICE_H_

View File

@@ -0,0 +1,30 @@
#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 generating a postal code for a location.
*/
#include <string>
#include "data_model/models.h"
/**
* @brief Interface for services that generate a postal code string matching
* a location's postal code format (`City::postal_regex`).
*/
class IPostalCodeService {
public:
virtual ~IPostalCodeService() = default;
/**
* @brief Generates a postal code for the given location.
*
* @param location Location whose postal code format to generate for.
* @return A postal code string matching `location.postal_regex`.
*/
virtual std::string GeneratePostalCode(const City& location) = 0;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_POSTAL_CODE_SERVICE_H_

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,7 @@ WORKDIR /app/build
COPY --from=builder /app/build/biergarten-pipeline ./
# Copy required config files
COPY locations.json /app/build/
COPY cities.json /app/build/
COPY beer-styles.json /app/build/
# Copy prompt templates

View File

@@ -7,16 +7,20 @@
#include <utility>
#include "services/postal_code/postal_code_service.h"
BiergartenPipelineOrchestrator::BiergartenPipelineOrchestrator(
std::shared_ptr<ILogger> logger,
std::unique_ptr<IEnrichmentService> context_service,
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)
: logger_(std::move(logger)),
context_service_(std::move(context_service)),
generator_(std::move(generator)),
exporter_(std::move(exporter)),
curated_data_service_(std::move(curated_data_service)),
postal_code_service_(std::move(postal_code_service)),
application_options_(application_options) {}

View File

@@ -22,12 +22,16 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
const auto generate_record =
[this, &skipped_count](
const Location& location,
const City& location,
const std::string& region_context) -> std::optional<BreweryRecord> {
try {
const BreweryResult brewery =
generator_->GenerateBrewery(location, region_context);
return BreweryRecord{.location = location, .brewery = brewery};
const std::string postal_code =
postal_code_service_->GeneratePostalCode(location);
return BreweryRecord{.location = location,
.address = Address{.postal_code = postal_code},
.brewery = brewery};
} catch (const std::exception& e) {
++skipped_count;

View File

@@ -14,12 +14,13 @@
void BiergartenPipelineOrchestrator::LogResults() const {
boost::json::array brewery_output;
for (const auto& [location, brewery] : generated_breweries_) {
for (const auto& [location, address, brewery] : generated_breweries_) {
brewery_output.push_back(boost::json::object{
{"name_en", brewery.name_en},
{"description_en", brewery.description_en},
{"name_local", brewery.name_local},
{"description_local", brewery.description_local},
{"postal_code", address.postal_code},
{"location", boost::json::object{
{"city", location.city},
{"country", location.country},

View File

@@ -14,14 +14,14 @@
#include "services/curated_data/curated_json_data_service.h"
#include "services/logging/logger.h"
std::vector<Location>
std::vector<City>
BiergartenPipelineOrchestrator::QueryCitiesWithCountries() {
logger_->Log({.level = LogLevel::Info,
.phase = PipelinePhase::Startup,
.message = "=== GEOGRAPHIC DATA OVERVIEW ==="});
const std::vector<Location>& all_locations =
curated_data_service_->LoadLocations();
const std::vector<City>& all_locations =
curated_data_service_->LoadCities();
const size_t sample_count = std::min(
static_cast<size_t>(application_options_.pipeline.location_count),
@@ -31,7 +31,7 @@ BiergartenPipelineOrchestrator::QueryCitiesWithCountries() {
static_cast<std::iter_difference_t<decltype(all_locations.cbegin())>>(
sample_count);
std::vector<Location> sampled_locations;
std::vector<City> sampled_locations;
sampled_locations.reserve(sample_count);
std::random_device random_generator;

View File

@@ -14,7 +14,7 @@ bool BiergartenPipelineOrchestrator::Run() {
try {
exporter_->Initialize();
std::vector<Location> cities = QueryCitiesWithCountries();
std::vector<City> cities = QueryCitiesWithCountries();
std::vector<EnrichedCity> enriched;
enriched.reserve(cities.size());

View File

@@ -36,7 +36,7 @@ static std::string FormatLocalLanguageCodes(
static constexpr int kBreweryInitialMaxTokens = 2800;
BreweryResult LlamaGenerator::GenerateBrewery(
const Location& location, const std::string& region_context) {
const City& location, const std::string& region_context) {
/**
* Preprocess and truncate region context to manageable size
*/

View File

@@ -8,14 +8,14 @@
#include "data_generation/mock_generator.h"
size_t MockGenerator::DeterministicHash(const Location& location) {
size_t MockGenerator::DeterministicHash(const City& location) {
size_t seed = 0;
boost::hash_combine(seed, location.city);
boost::hash_combine(seed, location.country);
return seed;
}
size_t MockGenerator::DeterministicHash(const Location& location,
size_t MockGenerator::DeterministicHash(const City& location,
const UserPersona& persona,
const Name& name) {
size_t seed = DeterministicHash(location);

View File

@@ -11,7 +11,7 @@
#include "data_generation/mock_generator.h"
BreweryResult MockGenerator::GenerateBrewery(
const Location& location, const std::string& /*region_context*/) {
const City& location, const std::string& /*region_context*/) {
const size_t hash = DeterministicHash(location);
const std::string_view adjective =

View File

@@ -102,12 +102,14 @@ int main(const int argc, char** argv) {
return std::make_unique<CuratedJsonDataService>(
CuratedDataFilePaths{
.locations_path = "locations.json",
.cities_path = "cities.json",
.personas_path = "personas.json",
.forenames_path = "forenames-by-country.json",
.surnames_path = "surnames-by-country.json",
});
}),
di::bind<IPostalCodeService>().to<MockPostalCodeService>(),
di::bind<IPromptFormatter>().to([options, log_producer] {
if (options.generator.use_mocked) {
{
@@ -226,4 +228,4 @@ int main(const int argc, char** argv) {
return shutdown(EXIT_FAILURE);
}
}
}

View File

@@ -1,6 +1,6 @@
/**
* @file json_handling/json_loader.cc
* @brief Parses curated location JSON input into strongly typed Location
* @brief Parses curated location JSON input into strongly typed City
* records with strict field validation and descriptive error reporting.
*/
@@ -109,31 +109,38 @@ std::string ReadFirstOfStringArray(const boost::json::object& object,
CuratedJsonDataService::CuratedJsonDataService(CuratedDataFilePaths filepaths)
: filepaths_(std::move(filepaths)) {}
const LocationsList& CuratedJsonDataService::LoadLocations() {
const CityList& CuratedJsonDataService::LoadCities() {
if (!cache_.locations.empty()) {
return cache_.locations;
}
const boost::json::value root =
ParseJsonFile(filepaths_.locations_path, "locations");
ParseJsonFile(filepaths_.cities_path, "cities");
if (!root.is_array()) {
throw std::runtime_error(
"Invalid locations JSON: root element must be an array");
"Invalid cities JSON: root element must be an array");
}
LocationsList locations;
CityList locations;
const auto& items = root.as_array();
locations.reserve(items.size());
for (const auto& item : items) {
if (!item.is_object()) {
throw std::runtime_error(
"Invalid locations JSON: each entry must be an object");
"Invalid cities JSON: each entry must be an object");
}
const auto& object = item.as_object();
locations.push_back(Location{
const boost::json::value* postal_code = object.if_contains("postal_code");
if (postal_code == nullptr || !postal_code->is_object()) {
throw std::runtime_error(
"Missing or invalid object field: postal_code");
}
locations.push_back(City{
.city = ReadRequiredString(object, "city"),
.state_province = ReadRequiredString(object, "state_province"),
.iso3166_2 = ReadRequiredString(object, "iso3166_2"),
@@ -142,8 +149,23 @@ const LocationsList& CuratedJsonDataService::LoadLocations() {
.local_languages = ReadRequiredStringArray(object, "local_languages"),
.latitude = ReadRequiredNumber(object, "latitude"),
.longitude = ReadRequiredNumber(object, "longitude"),
.postal_regex =
ReadRequiredStringArray(postal_code->as_object(), "city_regex"),
.postal_code_examples =
ReadRequiredStringArray(postal_code->as_object(), "examples"),
});
}
for (auto location : locations) {
std::cout << "Location: " << location.city << ", "
<< location.state_province << ", " << location.iso3166_2 << ", "
<< location.country << ", " << location.iso3166_1 << ", "
<< location.latitude << ", " << location.longitude << std::endl;
for (const auto& regex : location.postal_regex) {
std::cout << " Postal regex: " << regex << std::endl;
}
}
cache_.locations = std::move(locations);
return cache_.locations;
}

View File

@@ -7,38 +7,46 @@
MockCuratedDataService::MockCuratedDataService()
: locations_{
Location{.city = "Portland",
City{.city = "Portland",
.state_province = "Oregon",
.iso3166_2 = "US-OR",
.country = "United States",
.iso3166_1 = "US",
.local_languages = {"en"},
.latitude = 45.5152,
.longitude = -122.6784},
Location{.city = "Munich",
.longitude = -122.6784,
.postal_regex = {"^972[0-9]{2}(?:-[0-9]{4})?$"},
.postal_code_examples = {"97201", "97294"}},
City{.city = "Munich",
.state_province = "Bavaria",
.iso3166_2 = "DE-BY",
.country = "Germany",
.iso3166_1 = "DE",
.local_languages = {"de"},
.latitude = 48.1351,
.longitude = 11.5820},
Location{.city = "Lyon",
.longitude = 11.5820,
.postal_regex = {"^8[01][0-9]{3}$"},
.postal_code_examples = {"80331", "81929"}},
City{.city = "Lyon",
.state_province = "Auvergne-Rhone-Alpes",
.iso3166_2 = "FR-ARA",
.country = "France",
.iso3166_1 = "FR",
.local_languages = {"fr"},
.latitude = 45.7640,
.longitude = 4.8357},
Location{.city = "Brussels",
.longitude = 4.8357,
.postal_regex = {"^6900[1-9]$"},
.postal_code_examples = {"69001", "69009"}},
City{.city = "Brussels",
.state_province = "Brussels-Capital",
.iso3166_2 = "BE-BRU",
.country = "Belgium",
.iso3166_1 = "BE",
.local_languages = {"nl", "fr"},
.latitude = 50.8503,
.longitude = 4.3517},
.longitude = 4.3517,
.postal_regex = {"^1(?:0[0-9]{2}|1[0-9]{2}|20[0-9]|210)$"},
.postal_code_examples = {"1000", "1210"}},
},
personas_{
UserPersona{.name = "Hophead Explorer",
@@ -83,7 +91,7 @@ MockCuratedDataService::MockCuratedDataService()
{"BE", SurnameList{"Peeters", "Janssens"}},
} {}
const LocationsList& MockCuratedDataService::LoadLocations() {
const CityList& MockCuratedDataService::LoadCities() {
return locations_;
}

View File

@@ -11,7 +11,7 @@
#include "services/enrichment/wikipedia_service.h"
std::string WikipediaEnrichmentService::GetLocationContext(
const Location& loc) {
const City& loc) {
using namespace std::literals::chrono_literals;
if (!this->client_) {
if (logger_) {

View File

@@ -16,7 +16,7 @@ void SqliteExportService::Finalize() {
try {
insert_user_stmt_.reset();
insert_brewery_stmt_.reset();
insert_location_stmt_.reset();
insert_city_stmt_.reset();
if (transaction_open_) {
sqlite_export_service_internal::ExecSql(
db_handle_, "COMMIT;", "Failed to commit SQLite transaction");
@@ -24,7 +24,7 @@ void SqliteExportService::Finalize() {
}
db_handle_.reset();
location_cache_.clear();
city_cache_.clear();
} catch (...) {
RollbackAndCloseNoThrow();
throw;

View File

@@ -28,8 +28,8 @@ std::filesystem::path SqliteExportService::BuildDatabasePath() const {
void SqliteExportService::InitializeSchema() const {
sqlite_export_service_internal::ExecSql(
db_handle_, sqlite_export_service_internal::kCreateLocationsTableSql,
"Failed to create SQLite locations table");
db_handle_, sqlite_export_service_internal::kCreateCitiesTableSql,
"Failed to create SQLite cities table");
sqlite_export_service_internal::ExecSql(
db_handle_, sqlite_export_service_internal::kCreateBreweriesTableSql,
"Failed to create SQLite breweries table");
@@ -39,9 +39,9 @@ void SqliteExportService::InitializeSchema() const {
}
void SqliteExportService::PrepareStatements() {
insert_location_stmt_ = sqlite_export_service_internal::PrepareStatement(
db_handle_, sqlite_export_service_internal::kInsertLocationSql,
"Failed to prepare SQLite location insert statement");
insert_city_stmt_ = sqlite_export_service_internal::PrepareStatement(
db_handle_, sqlite_export_service_internal::kInsertCitySql,
"Failed to prepare SQLite city insert statement");
insert_brewery_stmt_ = sqlite_export_service_internal::PrepareStatement(
db_handle_, sqlite_export_service_internal::kInsertBrewerySql,
"Failed to prepare SQLite brewery insert statement");
@@ -62,9 +62,9 @@ void SqliteExportService::RollbackAndCloseNoThrow() noexcept {
insert_user_stmt_.reset();
insert_brewery_stmt_.reset();
insert_location_stmt_.reset();
insert_city_stmt_.reset();
db_handle_.reset();
location_cache_.clear();
city_cache_.clear();
}
void SqliteExportService::Initialize() {

View File

@@ -1,7 +1,7 @@
/**
* @file services/sqlite/process_record.cc
* @brief SqliteExportService::ProcessRecord() implementation
* and the shared location-resolution helper.
* and the shared city-resolution helper.
*/
#include <iomanip>
@@ -14,7 +14,7 @@
constexpr int kLocationPrecision = 17;
std::string SqliteExportService::BuildLocationKey(const Location& location) {
std::string SqliteExportService::BuildCityKey(const City& location) {
std::ostringstream key_stream;
key_stream << location.city << '\n'
<< location.state_province << '\n'
@@ -30,76 +30,76 @@ std::string SqliteExportService::BuildLocationKey(const Location& location) {
return key_stream.str();
}
sqlite3_int64 SqliteExportService::ResolveLocationId(const Location& location) {
const std::string location_key = BuildLocationKey(location);
const auto cached_location = location_cache_.find(location_key);
if (cached_location != location_cache_.end()) {
return cached_location->second;
sqlite3_int64 SqliteExportService::ResolveCityId(const City& location) {
const std::string city_key = BuildCityKey(location);
const auto cached_city = city_cache_.find(city_key);
if (cached_city != city_cache_.end()) {
return cached_city->second;
}
const std::string local_languages_json =
sqlite_export_service_internal::SerializeVector(location.local_languages);
sqlite_export_service_internal::Bind(
insert_location_stmt_,
insert_city_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{
.index = sqlite_export_service_internal::kLocationCityBindIndex,
.index = sqlite_export_service_internal::kCityNameBindIndex,
.value = location.city,
.action = "Failed to bind SQLite location city"});
.action = "Failed to bind SQLite city name"});
sqlite_export_service_internal::Bind(
insert_location_stmt_,
insert_city_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{
.index =
sqlite_export_service_internal::kLocationStateProvinceBindIndex,
sqlite_export_service_internal::kCityStateProvinceBindIndex,
.value = location.state_province,
.action = "Failed to bind SQLite location state/province"});
.action = "Failed to bind SQLite city state/province"});
sqlite_export_service_internal::Bind(
insert_location_stmt_,
insert_city_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{
.index = sqlite_export_service_internal::kLocationIso31662BindIndex,
.index = sqlite_export_service_internal::kCityIso31662BindIndex,
.value = location.iso3166_2,
.action = "Failed to bind SQLite location ISO 3166-2 code"});
.action = "Failed to bind SQLite city ISO 3166-2 code"});
sqlite_export_service_internal::Bind(
insert_location_stmt_,
insert_city_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{
.index = sqlite_export_service_internal::kLocationCountryBindIndex,
.index = sqlite_export_service_internal::kCityCountryBindIndex,
.value = location.country,
.action = "Failed to bind SQLite location country"});
.action = "Failed to bind SQLite city country"});
sqlite_export_service_internal::Bind(
insert_location_stmt_,
insert_city_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{
.index = sqlite_export_service_internal::kLocationIso31661BindIndex,
.index = sqlite_export_service_internal::kCityIso31661BindIndex,
.value = location.iso3166_1,
.action = "Failed to bind SQLite location ISO 3166-1 code"});
.action = "Failed to bind SQLite city ISO 3166-1 code"});
sqlite_export_service_internal::Bind(
insert_location_stmt_,
insert_city_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{
.index = sqlite_export_service_internal::kLocationLanguagesBindIndex,
.index = sqlite_export_service_internal::kCityLanguagesBindIndex,
.value = local_languages_json,
.action = "Failed to bind SQLite location languages"});
.action = "Failed to bind SQLite city languages"});
sqlite_export_service_internal::Bind(
insert_location_stmt_,
insert_city_stmt_,
sqlite_export_service_internal::BoundParam{
.index = sqlite_export_service_internal::kLocationLatitudeBindIndex,
.index = sqlite_export_service_internal::kCityLatitudeBindIndex,
.value = location.latitude,
.action = "Failed to bind SQLite location latitude"});
.action = "Failed to bind SQLite city latitude"});
sqlite_export_service_internal::Bind(
insert_location_stmt_,
insert_city_stmt_,
sqlite_export_service_internal::BoundParam{
.index = sqlite_export_service_internal::kLocationLongitudeBindIndex,
.index = sqlite_export_service_internal::kCityLongitudeBindIndex,
.value = location.longitude,
.action = "Failed to bind SQLite location longitude"});
.action = "Failed to bind SQLite city longitude"});
sqlite_export_service_internal::StepStatement(
db_handle_, insert_location_stmt_,
"Failed to insert SQLite location row");
db_handle_, insert_city_stmt_,
"Failed to insert SQLite city row");
const sqlite3_int64 location_id =
const sqlite3_int64 city_id =
sqlite_export_service_internal::LastInsertRowId(db_handle_);
location_cache_.emplace(location_key, location_id);
sqlite_export_service_internal::ResetStatement(insert_location_stmt_);
city_cache_.emplace(city_key, city_id);
sqlite_export_service_internal::ResetStatement(insert_city_stmt_);
return location_id;
return city_id;
}
uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
@@ -107,14 +107,14 @@ uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
throw std::runtime_error("SQLite export service is not initialized");
}
const sqlite3_int64 location_id = ResolveLocationId(brewery.location);
const sqlite3_int64 city_id = ResolveCityId(brewery.location);
sqlite_export_service_internal::Bind(
insert_brewery_stmt_,
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
.index = sqlite_export_service_internal::kBreweryLocationIdBindIndex,
.value = location_id,
.action = "Failed to bind SQLite brewery location id"});
.index = sqlite_export_service_internal::kBreweryCityIdBindIndex,
.value = city_id,
.action = "Failed to bind SQLite brewery city id"});
sqlite_export_service_internal::Bind(
insert_brewery_stmt_,
@@ -146,6 +146,13 @@ uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
.value = brewery.brewery.description_local,
.action = "Failed to bind SQLite brewery local description"});
sqlite_export_service_internal::Bind(
insert_brewery_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{
.index = sqlite_export_service_internal::kBreweryPostalCodeBindIndex,
.value = brewery.address.postal_code,
.action = "Failed to bind SQLite brewery postal code"});
sqlite_export_service_internal::StepStatement(
db_handle_, insert_brewery_stmt_, "Failed to insert SQLite brewery row");

View File

@@ -13,14 +13,14 @@ uint64_t SqliteExportService::ProcessRecord(const UserRecord& user) {
throw std::runtime_error("SQLite export service is not initialized");
}
const sqlite3_int64 location_id = ResolveLocationId(user.location);
const sqlite3_int64 city_id = ResolveCityId(user.location);
sqlite_export_service_internal::Bind(
insert_user_stmt_,
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
.index = sqlite_export_service_internal::kUserLocationIdBindIndex,
.value = location_id,
.action = "Failed to bind SQLite user location id"});
.index = sqlite_export_service_internal::kUserCityIdBindIndex,
.value = city_id,
.action = "Failed to bind SQLite user city id"});
sqlite_export_service_internal::Bind(
insert_user_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{