changeset 204:751ac58d7747

Irccd: initial properties required in Command, #516
author David Demelier <markand@malikania.fr>
date Fri, 10 Jun 2016 11:54:13 +0200
parents c26754e419c4
children eb5b0f480d63
files lib/irccd/cmd-plugin-config.cpp lib/irccd/cmd-plugin-config.hpp lib/irccd/cmd-plugin-info.cpp lib/irccd/cmd-plugin-info.hpp lib/irccd/cmd-plugin-load.cpp lib/irccd/cmd-plugin-load.hpp lib/irccd/cmd-plugin-reload.cpp lib/irccd/cmd-plugin-reload.hpp lib/irccd/command.cpp lib/irccd/command.hpp tests/CMakeLists.txt tests/command/CMakeLists.txt tests/command/main.cpp
diffstat 13 files changed, 338 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/lib/irccd/cmd-plugin-config.cpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/cmd-plugin-config.cpp	Fri Jun 10 11:54:13 2016 +0200
@@ -78,6 +78,11 @@
 	};
 }
 
+std::vector<Command::Property> PluginConfig::properties() const
+{
+	return {{ "plugin", { json::Type::String }}};
+}
+
 json::Value PluginConfig::request(Irccdctl &, const CommandRequest &args) const
 {
 	auto object = json::object({
@@ -96,6 +101,8 @@
 
 json::Value PluginConfig::exec(Irccd &irccd, const json::Value &request) const
 {
+	Command::exec(irccd, request);
+
 	return request.contains("value") ? execSet(irccd, request) : execGet(irccd, request);
 }
 
--- a/lib/irccd/cmd-plugin-config.hpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/cmd-plugin-config.hpp	Fri Jun 10 11:54:13 2016 +0200
@@ -51,6 +51,11 @@
 	IRCCD_EXPORT std::vector<Arg> args() const override;
 
 	/**
+	 * \copydoc Command::properties
+	 */
+	IRCCD_EXPORT std::vector<Property> properties() const override;
+
+	/**
 	 * \copydoc Command::request
 	 */
 	IRCCD_EXPORT json::Value request(Irccdctl &irccdctl, const CommandRequest &args) const override;
--- a/lib/irccd/cmd-plugin-info.cpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/cmd-plugin-info.cpp	Fri Jun 10 11:54:13 2016 +0200
@@ -43,6 +43,11 @@
 	return {{ "plugin", true }};
 }
 
+std::vector<Command::Property> PluginInfo::properties() const
+{
+	return {{ "plugin", { json::Type::String }}};
+}
+
 json::Value PluginInfo::request(Irccdctl &, const CommandRequest &args) const
 {
 	return json::object({{ "plugin", args.arg(0) }});
@@ -50,6 +55,8 @@
 
 json::Value PluginInfo::exec(Irccd &irccd, const json::Value &request) const
 {
+	Command::exec(irccd, request);
+
 	auto plugin = irccd.pluginService().require(request.at("plugin").toString());
 	
 	return json::object({
--- a/lib/irccd/cmd-plugin-info.hpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/cmd-plugin-info.hpp	Fri Jun 10 11:54:13 2016 +0200
@@ -52,6 +52,11 @@
 	IRCCD_EXPORT std::vector<Arg> args() const override;
 
 	/**
+	 * \copydoc Command::properties
+	 */
+	IRCCD_EXPORT std::vector<Property> properties() const override;
+
+	/**
 	 * \copydoc Command::request
 	 */
 	IRCCD_EXPORT json::Value request(Irccdctl &irccdctl, const CommandRequest &args) const override;
--- a/lib/irccd/cmd-plugin-load.cpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/cmd-plugin-load.cpp	Fri Jun 10 11:54:13 2016 +0200
@@ -40,13 +40,18 @@
 	return {{ "plugin", true }};
 }
 
+std::vector<Command::Property> PluginLoad::properties() const
+{
+	return {{ "plugin", { json::Type::String }}};
+}
+
 json::Value PluginLoad::exec(Irccd &irccd, const json::Value &request) const
 {
-	auto name = request.at("plugin").toString();
+	Command::exec(irccd, request);
 
-	irccd.pluginService().load(name);
+	irccd.pluginService().load(request.at("plugin").toString());
 
-	return Command::exec(irccd, request);
+	return json::object();
 }
 
 } // !command
--- a/lib/irccd/cmd-plugin-load.hpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/cmd-plugin-load.hpp	Fri Jun 10 11:54:13 2016 +0200
@@ -52,6 +52,11 @@
 	IRCCD_EXPORT std::vector<Arg> args() const override;
 
 	/**
+	 * \copydoc Command::properties
+	 */
+	IRCCD_EXPORT std::vector<Property> properties() const override;
+
+	/**
 	 * \copydoc Command::exec
 	 */
 	IRCCD_EXPORT json::Value exec(Irccd &irccd, const json::Value &request) const override;
--- a/lib/irccd/cmd-plugin-reload.cpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/cmd-plugin-reload.cpp	Fri Jun 10 11:54:13 2016 +0200
@@ -41,11 +41,18 @@
 	return {{ "plugin", true }};
 }
 
+std::vector<Command::Property> PluginReload::properties() const
+{
+	return {{ "plugin", { json::Type::String }}};
+}
+
 json::Value PluginReload::exec(Irccd &irccd, const json::Value &request) const
 {
+	Command::exec(irccd, request);
+
 	irccd.pluginService().require(request.at("plugin").toString())->onReload(irccd);
 
-	return Command::exec(irccd, request);
+	return json::object();
 }
 
 } // !command
--- a/lib/irccd/cmd-plugin-reload.hpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/cmd-plugin-reload.hpp	Fri Jun 10 11:54:13 2016 +0200
@@ -52,6 +52,11 @@
 	IRCCD_EXPORT std::vector<Arg> args() const override;
 
 	/**
+	 * \copydoc Command::properties
+	 */
+	IRCCD_EXPORT std::vector<Property> properties() const override;
+
+	/**
 	 * \copydoc Command::exec
 	 */
 	IRCCD_EXPORT json::Value exec(Irccd &irccd, const json::Value &request) const override;
--- a/lib/irccd/command.cpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/command.cpp	Fri Jun 10 11:54:13 2016 +0200
@@ -20,14 +20,65 @@
 #include <numeric>
 #include <sstream>
 
+#include <format.h>
+
 #include "command.hpp"
 #include "logger.hpp"
 #include "system.hpp"
 
 using namespace std::string_literals;
 
+using namespace fmt::literals;
+
 namespace irccd {
 
+namespace {
+
+/*
+ * typeName
+ * ------------------------------------------------------------------
+ *
+ * Convert a JSON value type to string for convenience.
+ */
+std::string typeName(json::Type type)
+{
+	static const std::vector<std::string> typenames{
+		"array", "boolean", "int", "null", "object", "real", "string"
+	};
+
+	assert(type >= json::Type::Array && type <= json::Type::String);
+
+	return typenames[static_cast<int>(type)];
+}
+
+/*
+ * typeNameList
+ * ------------------------------------------------------------------
+ *
+ * Construct a list of names to send a convenient error message if properties are invalid, example: string, int or bool expected.
+ */
+
+std::string typeNameList(const std::vector<json::Type> &types)
+{
+	std::ostringstream oss;
+
+	if (types.size() == 1)
+		return typeName(types[0]);
+
+	for (std::size_t i = 0; i < types.size(); ++i) {
+		oss << typeName(types[i]);
+
+		if (i == types.size() - 2)
+			oss << " or ";
+		else if (i < types.size() - 1)
+			oss << ", ";
+	}
+
+	return oss.str();
+}
+
+} // !namespace
+
 std::string Command::usage() const
 {
 	std::ostringstream oss;
@@ -90,8 +141,23 @@
 	return json::object({});
 }
 
-json::Value Command::exec(Irccd &, const json::Value &) const
+json::Value Command::exec(Irccd &, const json::Value &request) const
 {
+	// Verify that requested properties are present in the request.
+	for (const auto &prop : properties()) {
+		auto it = request.find(prop.name());
+
+		if (it == request.end())
+			throw std::invalid_argument("missing '{}' property"_format(prop.name()));
+
+		if (std::find(prop.types().begin(), prop.types().end(), it->typeOf()) == prop.types().end()) {
+			auto expected = typeNameList(prop.types());
+			auto got = typeName(it->typeOf());
+
+			throw std::invalid_argument("invalid '{}' property ({} expected, got {})"_format(prop.name(), expected, got));
+		}
+	}
+
 	return json::object({});
 }
 
--- a/lib/irccd/command.hpp	Thu Jun 09 13:50:55 2016 +0200
+++ b/lib/irccd/command.hpp	Fri Jun 10 11:54:13 2016 +0200
@@ -208,6 +208,11 @@
 	 */
 	class Arg;
 
+	/**
+	 * \brief Defines properties that must be available in the JSON request.
+	 */
+	class Property;
+
 private:
 	std::string m_name;
 	std::string m_category;
@@ -304,6 +309,19 @@
 	}
 
 	/**
+	 * Get the properties required in the JSON request.
+	 *
+	 * Default implementation returns empty list.
+	 *
+	 * \return the required properties
+	 * \note Put only **required** properties
+	 */
+	virtual std::vector<Property> properties() const
+	{
+		return {};
+	}
+
+	/**
 	 * Get the minimum number of arguments required.
 	 *
 	 * \return the minimum
@@ -495,6 +513,52 @@
 	}
 };
 
+/**
+ * \brief Property description for JSON request.
+ */
+class Command::Property {
+private:
+	std::string m_name;
+	std::vector<json::Type> m_types;
+
+public:
+	/**
+	 * Construct the property description.
+	 *
+	 * \pre !name.empty()
+	 * \pre types.size() >= 1
+	 * \param name the name
+	 * \param types the json types allowed
+	 */
+	inline Property(std::string name, std::vector<json::Type> types = { json::Type::String }) noexcept
+		: m_name(std::move(name))
+		, m_types(std::move(types))
+	{
+		assert(!m_name.empty());
+		assert(m_types.size() >= 1);
+	}
+
+	/**
+	 * Get the property name.
+	 *
+	 * \return the name
+	 */
+	inline const std::string &name() const noexcept
+	{
+		return m_name;
+	}
+
+	/**
+	 * Get the property types.
+	 *
+	 * \return the types
+	 */
+	inline const std::vector<json::Type> &types() const noexcept
+	{
+		return m_types;
+	}
+};
+
 } // !irccd
 
 #endif // !IRCCD_COMMAND_HPP
--- a/tests/CMakeLists.txt	Thu Jun 09 13:50:55 2016 +0200
+++ b/tests/CMakeLists.txt	Fri Jun 10 11:54:13 2016 +0200
@@ -21,6 +21,7 @@
 
 if (WITH_TESTS)
 	# Misc
+	add_subdirectory(command)
 	add_subdirectory(elapsedtimer)
 	add_subdirectory(logger)
 	add_subdirectory(path)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/command/CMakeLists.txt	Fri Jun 10 11:54:13 2016 +0200
@@ -0,0 +1,23 @@
+#
+# CMakeLists.txt -- CMake build system for irccd
+#
+# Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+irccd_define_test(
+	NAME command
+	SOURCES main.cpp
+	LIBRARIES libirccd
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/command/main.cpp	Fri Jun 10 11:54:13 2016 +0200
@@ -0,0 +1,133 @@
+/*
+ * main.cpp -- test Command class
+ *
+ * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <gtest/gtest.h>
+
+#include <irccd/command.hpp>
+
+using namespace irccd;
+
+class MyCommand : public Command {
+public:
+	MyCommand()
+		: Command("test", "Test")
+	{
+	}
+
+	std::string help() const override
+	{
+		return "This is a super command";
+	}
+
+	std::vector<Property> properties() const
+	{
+		return {
+			{ "b", { json::Type::Boolean			} },
+			{ "i", { json::Type::Int			} },
+			{ "m", { json::Type::Boolean, json::Type::Int, json::Type::String	} }
+		};
+	}
+};
+
+TEST(Properties, valid)
+{
+	Irccd *irccd = nullptr;
+	MyCommand cmd;
+
+	ASSERT_NO_THROW(cmd.exec(*irccd, json::object({
+		{ "b", true	},
+		{ "i", 123	},
+		{ "m", "abc"	}
+	})));
+
+	ASSERT_NO_THROW(cmd.exec(*irccd, json::object({
+		{ "b", true	},
+		{ "i", 123	},
+		{ "m", 456	}
+	})));
+
+	ASSERT_NO_THROW(cmd.exec(*irccd, json::object({
+		{ "b", true	},
+		{ "i", 123	},
+		{ "m", "456"	}
+	})));
+}
+
+TEST(Properties, missingB)
+{
+	Irccd *irccd = nullptr;
+	MyCommand cmd;
+
+	ASSERT_THROW(cmd.exec(*irccd, json::object({
+		{ "i", 123	},
+		{ "m", "abc"	}
+	})), std::invalid_argument);
+}
+
+TEST(Properties, missingI)
+{
+	Irccd *irccd = nullptr;
+	MyCommand cmd;
+
+	ASSERT_THROW(cmd.exec(*irccd, json::object({
+		{ "b", true	},
+		{ "m", "abc"	}
+	})), std::invalid_argument);
+}
+
+TEST(Properties, missingM)
+{
+	Irccd *irccd = nullptr;
+	MyCommand cmd;
+
+	ASSERT_THROW(cmd.exec(*irccd, json::object({
+		{ "b", true	},
+		{ "i", 123	},
+	})), std::invalid_argument);
+}
+
+TEST(Properties, invalidB)
+{
+	Irccd *irccd = nullptr;
+	MyCommand cmd;
+
+	ASSERT_THROW(cmd.exec(*irccd, json::object({
+		{ "b", "fail"	},
+		{ "i", 123	},
+		{ "m", "abc"	}
+	})), std::invalid_argument);
+}
+
+TEST(Properties, invalidM)
+{
+	Irccd *irccd = nullptr;
+	MyCommand cmd;
+
+	ASSERT_THROW(cmd.exec(*irccd, json::object({
+		{ "b", "fail"	},
+		{ "i", 123	},
+		{ "m", nullptr	}
+	})), std::invalid_argument);
+}
+
+int main(int argc, char **argv)
+{
+	testing::InitGoogleTest(&argc, argv);
+
+	return RUN_ALL_TESTS();
+}
\ No newline at end of file