Update: see also version 2
The Service
This code uses two API's.
List
https://LokiAstari.com/sample/list
https://LokiAstari.com/sample/list?token=<continuation Token>
This API returns a JSON object. The first version starts a list and will return an array of user ID (not all of them). The second version takes a token that was provided in a previous result and returns the next set of user ID's continuing from the previous location. If all users have been returned then the token is null.
{
result: [ <List of User ID> ],
token: "<token>" or null
}
Detail
https://LokiAstari.com/sample/detail/<User-ID>
This returns a JSON object with details of the user specified by the ID.
{
"id": <User ID: Number>,
"name": "<User Name: String>",
"age": <User Age: Number>,
"number": "<User Tel Number: String>",
"photo": "<User Image: URL(String)>",
"bio": "<User Bio: String>"
}
Notes:
It uses two of my other Libraries to make stuff simpler:
ThorsSerializer
Used to serialize JSON to/from C++ objects.
ThorsStream
Used to wrap CURL handle so that it looks like a std::istream.
Internally it uses MCURL handle to handle multiple CURL handles simultaneously with a single thread. A thread that reads from a stream that has an empty buffer will be released to do other work until there is data available in the buffer at which point that stream will become re-used.
What the app does:
The code finds the 5 youngest users who have valid US telephone numbers. It sorts the 5 Users by name and prints out the result.
A valid US telephone number is defined as:
<3 Digits><Sep><3 Digits><Sep><4 Digits>
Digit: 0-9
Sep: <space> or -
Code
#include <iostream>
#include <future>
#include <vector>
#include <string>
#include <memory>
#include <algorithm>
#include <regex>
#include <mutex>
#include "ThorSerialize/Traits.h"
#include "ThorSerialize/SerUtil.h"
#include "ThorSerialize/JsonThor.h"
#include "ThorsStream/ThorsStream.h"
using namespace std::string_literals;
// Some global constants.
const std::string api = "https://LokiAstari.com/sample"s;
const std::string apiList = api + "/list"s;
const std::string apiDetail = api + "/detail/"s;
const std::regex phoneNumber("^[0-9][0-9][0-9][- ][0-9][0-9][0-9][- ][0-9][0-9][0-9][0-9]$");
// In this app List and User
// are simply property bags no nead to have access functions.
// If this was a more complex app then we would consider having other methods.
struct List
{
std::vector<int> result;
std::unique_ptr<std::string> token;
};
struct User
{
int id;
std::string name;
int age;
std::string number;
std::string photo;
std::string bio;
};
// Set up comparison functions used on user.
const auto youngestUser = [](User const& lhs, User const& rhs){return lhs.age < rhs.age;};
const auto nameTest = [](User const& lhs, User const& rhs){return lhs.name < rhs.name;};
// Set up List and User to be read from JSON stream.
// See: jsonImport() and jsonExport() below
ThorsAnvil_MakeTrait(List, result, token);
ThorsAnvil_MakeTrait(User, id, name, age, number, photo, bio);
// A generic Job.
// Simply reads an object from an istream.
// If the read worked then processes it.
// Note: An istream treats a CURL socket like a standard C++ stream.
template<typename T>
class Job
{
ThorsAnvil::Stream::IThorStream istream;
public:
Job(std::string const& url)
: istream(url)
{}
virtual ~Job()
{}
void run(std::vector<User>& result)
{
using ThorsAnvil::Serialize::jsonImport;
T data;
if (istream >> jsonImport(data)) {
processesData(result, data);
}
else {
// Do some error handling
}
}
virtual void processesData(std::vector<User>& result, T const& data) = 0;
};
// A job to handle the details from getting a user object.
class UserJob: public Job<User>
{
public:
using Job<User>::Job;
virtual void processesData(std::vector<User>& users, User const& user) override
{
// Check if the phone number is OK.
if (std::regex_search(user.number, phoneNumber)) {
// Mutex shared across all objects (notice the static).
static std::mutex mutex;
// Lock the mutex when modifying "users"
std::lock_guard<std::mutex> lock(mutex);
// Add the user to a heap.
// The heap is ordered by youngest person.
users.emplace_back(std::move(user));
std::push_heap(users.begin(), users.end(), youngestUser);
if (users.size() == 6) {
// If we have more than 5 people the pop the oldest one off.
// Thus we maintain a heap of the 5 youngest people.
std::pop_heap(users.begin(), users.end(), youngestUser);
users.pop_back();
}
}
}
};
// A job to handle the list object.
class ListJob: public Job<List>
{
public:
using Job<List>::Job;
virtual void processesData(std::vector<User>& users, List const& data) override
{
if (data.token.get()) {
// If we have a continuation token
// Then add another job ("ListJob") to the async queue.
std::async([&users, job = std::make_unique<ListJob>(apiList + "?token=" + *data.token)](){job->run(users);});
}
for(auto const& userId: data.result) {
// For each user add a job ("UserJob") to the async queue.
std::async([&users, job = std::make_unique<UserJob>(apiDetail + std::to_string(userId))](){job->run(users);});
}
}
};
int main()
{
std::vector<User> users;
std::async([&users, job = std::make_unique<ListJob>(apiList)](){job->run(users);});
// This will not return until all async jobs have completed.
std::sort(users.begin(), users.end(), nameTest);
using ThorsAnvil::Serialize::jsonExport;
std::cout << jsonExport(users) << "\n";
}