This query string parser is intended to parse the parameters and values of a parser query string. I'm particularly concerned about my use of std::move(...)
.
I haven't used C++ almost 15 years and have never done so professionally, so any and all feedback is welcome.
HttpUtility.h:
class UrlUtil final
{
public:
static std::string urlDecode(const std::string &urlEncodedString);
};
class QueryParameter final
{
public:
const std::string &name() const { return name_; }
const std::string &firstValue() const;
//Question 1: because this is a const&, this is not bad practice, correct?
const std::vector<std::string> &allValues() const { return values_; }
int valueCount() const { return values_.size(); }
QueryParameter(const std::string name) : name_(std::move(name)) { }
private:
friend class QueryString;
void addValue(std::string value) { values_.push_back(value); }
std::string name_;
std::vector<std::string> values_;
};
class QueryString final
{
public:
QueryString(std::istream &input);
int numParams() const { return parameters_.size(); }
bool hasParam(const char *name) const { return parameters_.find(std::string(name)) != parameters_.end(); }
std::vector<std::string> getParamNames() const;
//Question 2: Instead of returning a vector<string>, would something like this be better?
//void fillParamNames(const std::vector<std::string> into) const;
const QueryParameter *getParam(const std::string &name) const;
const QueryParameter *getParam(const char* name) const { return getParam(std::string(name)); };
private:
static std::string parseName(std::istream &input);
static std::string parseValue(std::istream &input);
std::map<std::string, std::unique_ptr<QueryParameter>> parameters_;
};
HttpUtility.cpp:
#include "HttpUtility.h"
#include <sstream>
/*
* UrlUtil Methods
*/
//Note: static method
std::string UrlUtil::urlDecode(const std::string &urlEncodedString)
{
std::stringstream input(urlEncodedString);
std::string decodedString = "";
while(!input.eof())
{
int c = input.get();
if(c < 0 || c == '&')
break;
if(c == '+')
{
decodedString.push_back(' ');
}
else if(c == '%')
{
char octalCode[3];
octalCode[0] = (char)input.get();
octalCode[1] = (char)input.get();
octalCode[2] = 0;
int ch = std::stoi(octalCode, 0, 16);
decodedString.push_back(ch);
}
else
decodedString.push_back(c);
}
return std::move(decodedString);
}
/*
* QueryParameter Methods
*/
const std::string &QueryParameter::firstValue() const
{
if(valueCount() == 0)
//Question 3: I am really not sure of the appropriate exception to throw here...
throw std::runtime_error("Attempted to obtain first value of query string parameter that does not have any values");
return values_[0];
}
/*
* QueryString Methods
*/
QueryString::QueryString(std::istream &input)
{
//Example: param1=value1¶m2=value2
while (!input.eof() && input.peek() > 0)
{
//Should eat "param1="
auto name = parseName(input);
//Should eat value1&
std::string value = parseValue(input);
auto foundItr = parameters_.find(name);
if(foundItr == parameters_.end())
{
auto newParam = std::make_unique<QueryParameter>(name);
if(value.size() > 0)
{
newParam->addValue(value);
}
parameters_.emplace(name, std::move(newParam)).first;
}
else
(*foundItr).second->addValue(value);
}
}
std::vector<std::string> QueryString::getParamNames() const
{
std::vector<std::string> names;
for(auto const& pair : parameters_)
names.push_back(pair.second->name());
return std::move(names);
}
const QueryParameter *QueryString::getParam(const std::string &name) const
{
auto itr = parameters_.find(name);
return itr == parameters_.end() ? nullptr : (*itr).second.get();
};
//Note: static method
std::string QueryString::parseName(std::istream &input)
{
std::string name = "";
while(!input.eof() && input.peek()!= '=')
name.push_back(input.get());
//Eat the '='
if(!input.eof())
input.get();
return name;
}
//Note: static method
std::string QueryString::parseValue(std::istream &input)
{
std::string urlEncodedValue;
int c = input.get();
while(c > 0 && c != '&')
{
urlEncodedValue.push_back(c);
c = input.get();
}
if(urlEncodedValue.size() == 0)
return "";
std::string decodedValue = std::move(UrlUtil::urlDecode(urlEncodedValue));
return std::move(decodedValue);
}
tests.cpp:
#define BOOST_TEST_MODULE cgi-test-tests
#define BOOST_TEST_DYN_LINK
#include <string>
#include <sstream>
#include <boost/test/unit_test.hpp>
#include "HttpUtility.h"
using namespace std;
BOOST_AUTO_TEST_CASE(UrlUtil_urlDecode_works)
{
string decoded = UrlUtil::urlDecode("testing+space");
BOOST_CHECK_EQUAL(decoded, "testing space");
decoded = UrlUtil::urlDecode("!%40%23%24%25%5E%26*()_%2B");
BOOST_CHECK_EQUAL(decoded, "!@#$%^&*()_+");
}
BOOST_AUTO_TEST_CASE(QueryString_can_parse_query_string_with_no_value_ending_with_eof)
{
stringstream stream("name1=");
QueryString qs(stream);
BOOST_CHECK(qs.numParams() == 1);
BOOST_CHECK_EQUAL(qs.hasParam("name1"), true);
auto p = qs.getParam("name1");
BOOST_CHECK_NE(p, (QueryParameter*)nullptr);
BOOST_CHECK_EQUAL(p->name(), "name1");
BOOST_CHECK_EQUAL(p->valueCount(), 0);
}
BOOST_AUTO_TEST_CASE(QueryString_can_parse_query_string_with_no_value_ending_with_amp)
{
stringstream stream("name1=&");
QueryString qs(stream);
BOOST_CHECK(qs.numParams() == 1);
BOOST_CHECK_EQUAL(qs.hasParam("name1"), true);
auto p = qs.getParam("name1");
BOOST_CHECK_NE(p, (QueryParameter*)nullptr);
BOOST_CHECK_EQUAL(p->name(), "name1");
BOOST_CHECK_EQUAL(p->valueCount(), 0);
}
BOOST_AUTO_TEST_CASE(QueryString_can_parse_query_string_with_one_parameter)
{
stringstream stream("name1=value1");
QueryString qs(stream);
BOOST_CHECK(qs.numParams() == 1);
BOOST_CHECK_EQUAL(qs.hasParam("name1"), true);
auto p = qs.getParam("name1");
BOOST_CHECK_NE(p, (QueryParameter*)nullptr);
BOOST_CHECK_EQUAL(p->name(), "name1");
BOOST_CHECK_EQUAL(p->valueCount(), 1);
BOOST_CHECK_EQUAL(p->firstValue(), "value1");
}
BOOST_AUTO_TEST_CASE(QueryString_can_parse_query_string_with_one_parameter_multiple_values)
{
stringstream stream("name1=value1&name1=value2");
QueryString qs(stream);
BOOST_CHECK(qs.numParams() == 1);
BOOST_CHECK_EQUAL(qs.hasParam("name1"), true);
auto p = qs.getParam("name1");
BOOST_CHECK_NE(p, (QueryParameter*)nullptr);
BOOST_CHECK_EQUAL(p->name(), "name1");
BOOST_CHECK_EQUAL(p->valueCount(), 2);
auto values = p->allValues();
BOOST_CHECK_EQUAL(values[0], "value1");
BOOST_CHECK_EQUAL(values[1], "value2");
}
BOOST_AUTO_TEST_CASE(QueryString_can_parse_query_string_with_two_parameters)
{
stringstream stream("name1=value1&name2=value2&");
QueryString qs(stream);
BOOST_CHECK(qs.numParams() == 2);
//first parameter
BOOST_CHECK_EQUAL(qs.hasParam("name1"), true);
auto p = qs.getParam("name1");
BOOST_CHECK_NE(p, (QueryParameter*)nullptr);
BOOST_CHECK_EQUAL(p->name(), "name1");
BOOST_CHECK_EQUAL(p->valueCount(), 1);
BOOST_CHECK_EQUAL(p->firstValue(), "value1");
//second parameter
BOOST_CHECK_EQUAL(qs.hasParam("name2"), true);
p = qs.getParam("name2");
BOOST_CHECK_NE(p, (QueryParameter*)nullptr);
BOOST_CHECK_EQUAL(p->name(), "name2");
BOOST_CHECK_EQUAL(p->valueCount(), 1);
BOOST_CHECK_EQUAL(p->firstValue(), "value2");
}
std::move()
might be superfluous, since RVO is supported by default for most of the modern compilers. \$\endgroup\$