blob: b70beffac7978136bd2df2160e65c584830c8930 [file] [log] [blame]
// 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/object_schema.h"
#include <algorithm>
#include <limits>
#include <base/logging.h>
#include <base/values.h>
#include <weave/enum_to_string.h>
#include "src/commands/prop_types.h"
#include "src/commands/prop_values.h"
#include "src/commands/schema_constants.h"
#include "src/string_utils.h"
namespace weave {
namespace {
// Helper function for to create a PropType based on type string.
// Generates an error if the string identifies an unknown type.
std::unique_ptr<PropType> CreatePropType(const std::string& type_name,
ErrorPtr* error) {
auto parts = SplitAtFirst(type_name, ".", false);
const std::string& primary_type = parts.first;
const std::string& array_type = parts.second;
std::unique_ptr<PropType> prop;
ValueType type;
if (PropType::GetTypeFromTypeString(primary_type, &type)) {
prop = PropType::Create(type);
if (prop && type == ValueType::Array && !array_type.empty()) {
auto items_type = CreatePropType(array_type, error);
if (items_type) {
prop->GetArray()->SetItemType(std::move(items_type));
} else {
prop.reset();
return prop;
}
}
}
if (!prop) {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kUnknownType, "Unknown type %s",
type_name.c_str());
}
return prop;
}
// Generates "no_type_info" error.
void ErrorInvalidTypeInfo(ErrorPtr* error) {
Error::AddTo(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kNoTypeInfo,
"Unable to determine parameter type");
}
// Helper function for PropFromJson to handle the case of parameter being
// defined as a JSON string like this:
// "prop":"..."
std::unique_ptr<PropType> PropFromJsonString(const base::Value& value,
const PropType* base_schema,
ErrorPtr* error) {
std::string type_name;
CHECK(value.GetAsString(&type_name)) << "Unable to get string value";
std::unique_ptr<PropType> prop = CreatePropType(type_name, error);
base::DictionaryValue empty;
if (prop && !prop->FromJson(&empty, base_schema, error))
prop.reset();
return prop;
}
// Detects a type based on JSON array. Inspects the first element of the array
// to deduce the PropType from. Returns the string name of the type detected
// or empty string is type detection failed.
std::string DetectArrayType(const base::ListValue* list,
const PropType* base_schema,
bool allow_arrays) {
std::string type_name;
if (base_schema) {
type_name = base_schema->GetTypeAsString();
} else if (list->GetSize() > 0) {
const base::Value* first_element = nullptr;
if (list->Get(0, &first_element)) {
switch (first_element->GetType()) {
case base::Value::TYPE_BOOLEAN:
type_name = PropType::GetTypeStringFromType(ValueType::Boolean);
break;
case base::Value::TYPE_INTEGER:
type_name = PropType::GetTypeStringFromType(ValueType::Int);
break;
case base::Value::TYPE_DOUBLE:
type_name = PropType::GetTypeStringFromType(ValueType::Double);
break;
case base::Value::TYPE_STRING:
type_name = PropType::GetTypeStringFromType(ValueType::String);
break;
case base::Value::TYPE_DICTIONARY:
type_name = PropType::GetTypeStringFromType(ValueType::Object);
break;
case base::Value::TYPE_LIST: {
if (allow_arrays) {
type_name = PropType::GetTypeStringFromType(ValueType::Array);
const base::ListValue* first_element_list = nullptr;
if (first_element->GetAsList(&first_element_list)) {
// We do not allow arrays of arrays.
auto child_type =
DetectArrayType(first_element_list, nullptr, false);
if (child_type.empty()) {
type_name.clear();
} else {
type_name += '.' + child_type;
}
}
}
break;
}
default:
// The rest are unsupported.
break;
}
}
}
return type_name;
}
// Helper function for PropFromJson to handle the case of parameter being
// defined as a JSON array like this:
// "prop":[...]
std::unique_ptr<PropType> PropFromJsonArray(const base::Value& value,
const PropType* base_schema,
ErrorPtr* error) {
std::unique_ptr<PropType> prop;
const base::ListValue* list = nullptr;
CHECK(value.GetAsList(&list)) << "Unable to get array value";
std::string type_name = DetectArrayType(list, base_schema, true);
if (type_name.empty()) {
ErrorInvalidTypeInfo(error);
return prop;
}
base::DictionaryValue array_object;
array_object.SetWithoutPathExpansion(commands::attributes::kOneOf_Enum,
list->DeepCopy());
prop = CreatePropType(type_name, error);
if (prop && !prop->FromJson(&array_object, base_schema, error))
prop.reset();
return prop;
}
// Detects a type based on JSON object definition of type. Looks at various
// members such as minimum/maximum constraints, default and enum values to
// try to deduce the underlying type of the element. Returns the string name of
// the type detected or empty string is type detection failed.
std::string DetectObjectType(const base::DictionaryValue* dict,
const PropType* base_schema) {
bool has_min_max = dict->HasKey(commands::attributes::kNumeric_Min) ||
dict->HasKey(commands::attributes::kNumeric_Max);
// Here we are trying to "detect the type and read in the object based on
// the deduced type". Later, we'll verify that this detected type matches
// the expectation of the base schema, if applicable, to make sure we are not
// changing the expected type. This makes the vendor-side (re)definition of
// standard and custom commands behave exactly the same.
// The only problem with this approach was the double-vs-int types.
// If the type is meant to be a double we want to allow its definition as
// "min:0, max:0" instead of just forcing it to be only "min:0.0, max:0.0".
// If we have "minimum" or "maximum", and we have a Double schema object,
// treat this object as a Double (even if both min and max are integers).
if (has_min_max && base_schema && base_schema->GetType() == ValueType::Double)
return PropType::GetTypeStringFromType(ValueType::Double);
// If we have at least one "minimum" or "maximum" that is Double,
// it's a Double.
const base::Value* value = nullptr;
if (dict->Get(commands::attributes::kNumeric_Min, &value) &&
value->IsType(base::Value::TYPE_DOUBLE))
return PropType::GetTypeStringFromType(ValueType::Double);
if (dict->Get(commands::attributes::kNumeric_Max, &value) &&
value->IsType(base::Value::TYPE_DOUBLE))
return PropType::GetTypeStringFromType(ValueType::Double);
// If we have "minimum" or "maximum", it's an Integer.
if (has_min_max)
return PropType::GetTypeStringFromType(ValueType::Int);
// If we have "minLength" or "maxLength", it's a String.
if (dict->HasKey(commands::attributes::kString_MinLength) ||
dict->HasKey(commands::attributes::kString_MaxLength))
return PropType::GetTypeStringFromType(ValueType::String);
// If we have "properties", it's an object.
if (dict->HasKey(commands::attributes::kObject_Properties))
return PropType::GetTypeStringFromType(ValueType::Object);
// If we have "items", it's an array.
if (dict->HasKey(commands::attributes::kItems))
return PropType::GetTypeStringFromType(ValueType::Array);
// If we have "enum", it's an array. Detect type from array elements.
const base::ListValue* list = nullptr;
if (dict->GetListWithoutPathExpansion(commands::attributes::kOneOf_Enum,
&list))
return DetectArrayType(list, base_schema, true);
// If we have "default", try to use it for type detection.
if (dict->Get(commands::attributes::kDefault, &value)) {
if (value->IsType(base::Value::TYPE_DOUBLE))
return PropType::GetTypeStringFromType(ValueType::Double);
if (value->IsType(base::Value::TYPE_INTEGER))
return PropType::GetTypeStringFromType(ValueType::Int);
if (value->IsType(base::Value::TYPE_BOOLEAN))
return PropType::GetTypeStringFromType(ValueType::Boolean);
if (value->IsType(base::Value::TYPE_STRING))
return PropType::GetTypeStringFromType(ValueType::String);
if (value->IsType(base::Value::TYPE_LIST)) {
CHECK(value->GetAsList(&list)) << "List value expected";
std::string child_type = DetectArrayType(list, base_schema, false);
if (!child_type.empty()) {
return PropType::GetTypeStringFromType(ValueType::Array) + '.' +
child_type;
}
}
}
return std::string{};
}
// Helper function for PropFromJson to handle the case of parameter being
// defined as a JSON object like this:
// "prop":{...}
std::unique_ptr<PropType> PropFromJsonObject(const base::Value& value,
const PropType* base_schema,
ErrorPtr* error) {
std::unique_ptr<PropType> prop;
const base::DictionaryValue* dict = nullptr;
CHECK(value.GetAsDictionary(&dict)) << "Unable to get dictionary value";
std::string type_name;
if (dict->HasKey(commands::attributes::kType)) {
if (!dict->GetString(commands::attributes::kType, &type_name)) {
ErrorInvalidTypeInfo(error);
return prop;
}
} else {
type_name = DetectObjectType(dict, base_schema);
}
if (type_name.empty()) {
if (!base_schema) {
ErrorInvalidTypeInfo(error);
return prop;
}
type_name = base_schema->GetTypeAsString();
}
prop = CreatePropType(type_name, error);
if (prop && !prop->FromJson(dict, base_schema, error))
prop.reset();
return prop;
}
const EnumToStringMap<base::Value::Type>::Map kMap[] = {
{base::Value::TYPE_NULL, "Null"},
{base::Value::TYPE_BOOLEAN, "Boolean"},
{base::Value::TYPE_INTEGER, "Integer"},
{base::Value::TYPE_DOUBLE, "Double"},
{base::Value::TYPE_STRING, "String"},
{base::Value::TYPE_BINARY, "Binary"},
{base::Value::TYPE_DICTIONARY, "Object"},
{base::Value::TYPE_LIST, "Array"},
};
} // anonymous namespace
template <>
EnumToStringMap<base::Value::Type>::EnumToStringMap() : EnumToStringMap(kMap) {}
ObjectSchema::ObjectSchema() {}
ObjectSchema::~ObjectSchema() {}
std::unique_ptr<ObjectSchema> ObjectSchema::Clone() const {
std::unique_ptr<ObjectSchema> cloned{new ObjectSchema};
for (const auto& pair : properties_) {
cloned->properties_.emplace(pair.first, pair.second->Clone());
}
cloned->extra_properties_allowed_ = extra_properties_allowed_;
return cloned;
}
void ObjectSchema::AddProp(const std::string& name,
std::unique_ptr<PropType> prop) {
// Not using emplace() here to make sure we override existing properties.
properties_[name] = std::move(prop);
}
const PropType* ObjectSchema::GetProp(const std::string& name) const {
auto p = properties_.find(name);
return p != properties_.end() ? p->second.get() : nullptr;
}
bool ObjectSchema::MarkPropRequired(const std::string& name, ErrorPtr* error) {
auto p = properties_.find(name);
if (p == properties_.end()) {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kUnknownProperty,
"Unknown property '%s'", name.c_str());
return false;
}
p->second->MakeRequired(true);
return true;
}
std::unique_ptr<base::DictionaryValue> ObjectSchema::ToJson(
bool full_schema,
bool in_command_def) const {
std::unique_ptr<base::DictionaryValue> value(new base::DictionaryValue);
for (const auto& pair : properties_) {
auto prop_def = pair.second->ToJson(full_schema, in_command_def);
CHECK(prop_def);
value->SetWithoutPathExpansion(pair.first, prop_def.release());
}
return value;
}
bool ObjectSchema::FromJson(const base::DictionaryValue* value,
const ObjectSchema* object_schema,
ErrorPtr* error) {
Properties properties;
base::DictionaryValue::Iterator iter(*value);
while (!iter.IsAtEnd()) {
std::string name = iter.key();
const PropType* base_schema =
object_schema ? object_schema->GetProp(iter.key()) : nullptr;
auto prop_type = PropFromJson(iter.value(), base_schema, error);
if (prop_type) {
properties.emplace(iter.key(), std::move(prop_type));
} else {
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kInvalidPropDef,
"Error in definition of property '%s'",
iter.key().c_str());
return false;
}
iter.Advance();
}
properties_ = std::move(properties);
return true;
}
std::unique_ptr<PropType> ObjectSchema::PropFromJson(
const base::Value& value,
const PropType* base_schema,
ErrorPtr* error) {
if (value.IsType(base::Value::TYPE_STRING)) {
// A string value is a short-hand object specification and provides
// the parameter type.
return PropFromJsonString(value, base_schema, error);
} else if (value.IsType(base::Value::TYPE_LIST)) {
// One of the enumerated types.
return PropFromJsonArray(value, base_schema, error);
} else if (value.IsType(base::Value::TYPE_DICTIONARY)) {
// Full parameter definition.
return PropFromJsonObject(value, base_schema, error);
}
Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
errors::commands::kUnknownType,
"Unexpected JSON value type: %s",
EnumToString(value.GetType()).c_str());
return nullptr;
}
std::unique_ptr<ObjectSchema> ObjectSchema::Create() {
return std::unique_ptr<ObjectSchema>{new ObjectSchema};
}
} // namespace weave