blob: 053e7aa1644412756022d99f2cd0e3243213060c [file] [log] [blame] [edit]
// Copyright 2015 The Weave Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/commands/command_dictionary.h"
#include <base/values.h>
#include <weave/enum_to_string.h>
#include "src/commands/command_definition.h"
#include "src/commands/schema_constants.h"
#include "src/string_utils.h"
namespace weave {
bool CommandDictionary::LoadCommands(const base::DictionaryValue& json,
const CommandDictionary* base_commands,
ErrorPtr* error) {
CommandMap new_defs;
// |json| contains a list of nested objects with the following structure:
// {"<pkg_name>": {"<cmd_name>": {"parameters": {object_schema}}, ...}, ...}
// Iterate over packages
base::DictionaryValue::Iterator package_iter(json);
while (!package_iter.IsAtEnd()) {
std::string package_name = package_iter.key();
const base::DictionaryValue* package_value = nullptr;
if (!package_iter.value().GetAsDictionary(&package_value)) {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kTypeMismatch,
"Expecting an object for package '%s'",
package_name.c_str());
return false;
}
// Iterate over command definitions within the current package.
base::DictionaryValue::Iterator command_iter(*package_value);
while (!command_iter.IsAtEnd()) {
std::string command_name = command_iter.key();
if (command_name.empty()) {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kInvalidCommandName,
"Unnamed command encountered in package '%s'",
package_name.c_str());
return false;
}
const base::DictionaryValue* command_def_json = nullptr;
if (!command_iter.value().GetAsDictionary(&command_def_json)) {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kTypeMismatch,
"Expecting an object for command '%s'",
command_name.c_str());
return false;
}
// Construct the compound command name as "pkg_name.cmd_name".
std::string full_command_name = Join(".", package_name, command_name);
const ObjectSchema* base_parameters_def = nullptr;
const ObjectSchema* base_progress_def = nullptr;
const ObjectSchema* base_results_def = nullptr;
// By default make it available to all clients.
auto visibility = CommandDefinition::Visibility::GetAll();
UserRole minimal_role{UserRole::kUser};
if (base_commands) {
auto cmd = base_commands->FindCommand(full_command_name);
if (cmd) {
base_parameters_def = cmd->GetParameters();
base_progress_def = cmd->GetProgress();
base_results_def = cmd->GetResults();
visibility = cmd->GetVisibility();
minimal_role = cmd->GetMinimalRole();
}
// If the base command dictionary was provided but the command was not
// found in it, this must be a custom (vendor) command. GCD spec states
// that all custom command names must begin with "_". Let's enforce
// this rule here.
if (!cmd) {
if (command_name.front() != '_') {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kInvalidCommandName,
"The name of custom command '%s' in package '%s'"
" must start with '_'",
command_name.c_str(), package_name.c_str());
return false;
}
}
}
auto parameters_schema = BuildObjectSchema(
command_def_json, commands::attributes::kCommand_Parameters,
base_parameters_def, full_command_name, error);
if (!parameters_schema)
return false;
auto progress_schema = BuildObjectSchema(
command_def_json, commands::attributes::kCommand_Progress,
base_progress_def, full_command_name, error);
if (!progress_schema)
return false;
auto results_schema = BuildObjectSchema(
command_def_json, commands::attributes::kCommand_Results,
base_results_def, full_command_name, error);
if (!results_schema)
return false;
std::string value;
if (command_def_json->GetString(commands::attributes::kCommand_Visibility,
&value)) {
if (!visibility.FromString(value, error)) {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kInvalidCommandVisibility,
"Error parsing command '%s'",
full_command_name.c_str());
return false;
}
}
if (command_def_json->GetString(commands::attributes::kCommand_Role,
&value)) {
if (!StringToEnum(value, &minimal_role)) {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kInvalidPropValue,
"Invalid role: '%s'", value.c_str());
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kInvalidMinimalRole,
"Error parsing command '%s'",
full_command_name.c_str());
return false;
}
}
std::unique_ptr<CommandDefinition> command_def{new CommandDefinition{
std::move(parameters_schema), std::move(progress_schema),
std::move(results_schema)}};
command_def->SetVisibility(visibility);
command_def->SetMinimalRole(minimal_role);
new_defs.emplace(full_command_name, std::move(command_def));
command_iter.Advance();
}
package_iter.Advance();
}
// Verify that newly loaded command definitions do not override existing
// definitions in another category. This is unlikely, but we don't want to let
// one vendor daemon to define the same commands already handled by another
// daemon on the same device.
for (const auto& pair : new_defs) {
auto iter = definitions_.find(pair.first);
CHECK(iter == definitions_.end()) << "Definition for command '"
<< pair.first
<< "' overrides an earlier definition";
}
// Insert new definitions into the global map.
for (auto& pair : new_defs)
definitions_.emplace(pair.first, std::move(pair.second));
return true;
}
std::unique_ptr<ObjectSchema> CommandDictionary::BuildObjectSchema(
const base::DictionaryValue* command_def_json,
const char* property_name,
const ObjectSchema* base_def,
const std::string& command_name,
ErrorPtr* error) {
auto object_schema = ObjectSchema::Create();
const base::DictionaryValue* schema_def = nullptr;
if (!command_def_json->GetDictionaryWithoutPathExpansion(property_name,
&schema_def)) {
if (base_def)
return base_def->Clone();
return object_schema;
}
if (!object_schema->FromJson(schema_def, base_def, error)) {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kInvalidObjectSchema,
"Invalid definition for command '%s'",
command_name.c_str());
return {};
}
return object_schema;
}
std::unique_ptr<base::DictionaryValue> CommandDictionary::GetCommandsAsJson(
const std::function<bool(const CommandDefinition*)>& filter,
bool full_schema,
ErrorPtr* error) const {
std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue);
for (const auto& pair : definitions_) {
// Check if the command definition has the desired visibility.
// If not, then skip it.
if (!filter(pair.second.get()))
continue;
std::unique_ptr<base::DictionaryValue> parameters =
pair.second->GetParameters()->ToJson(full_schema, true);
CHECK(parameters);
// Progress and results are not part of public commandDefs.
auto parts = SplitAtFirst(pair.first, ".", true);
const std::string& package_name = parts.first;
const std::string& command_name = parts.second;
base::DictionaryValue* package = nullptr;
if (!dict->GetDictionaryWithoutPathExpansion(package_name, &package)) {
// If this is the first time we encounter this package, create a JSON
// object for it.
package = new base::DictionaryValue;
dict->SetWithoutPathExpansion(package_name, package);
}
base::DictionaryValue* command_def = new base::DictionaryValue;
command_def->Set(commands::attributes::kCommand_Parameters,
parameters.release());
command_def->SetString(commands::attributes::kCommand_Role,
EnumToString(pair.second->GetMinimalRole()));
package->SetWithoutPathExpansion(command_name, command_def);
}
return dict;
}
const CommandDefinition* CommandDictionary::FindCommand(
const std::string& command_name) const {
auto pair = definitions_.find(command_name);
return (pair != definitions_.end()) ? pair->second.get() : nullptr;
}
CommandDefinition* CommandDictionary::FindCommand(
const std::string& command_name) {
auto pair = definitions_.find(command_name);
return (pair != definitions_.end()) ? pair->second.get() : nullptr;
}
void CommandDictionary::Clear() {
definitions_.clear();
}
} // namespace weave