blob: 08410cc3b31cd57eb4d994b868c9ea123be6362b [file] [log] [blame]
// Copyright 2014 The Chromium OS 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 "buffet/commands/prop_types.h"
#include <algorithm>
#include <limits>
#include <set>
#include <base/json/json_writer.h>
#include <base/logging.h>
#include <base/values.h>
#include <chromeos/any.h>
#include <chromeos/strings/string_utils.h>
#include "buffet/commands/object_schema.h"
#include "buffet/commands/prop_values.h"
#include "buffet/commands/schema_constants.h"
namespace buffet {
// PropType -------------------------------------------------------------------
PropType::PropType() {
}
PropType::~PropType() {
}
std::string PropType::GetTypeAsString() const {
return GetTypeStringFromType(GetType());
}
bool PropType::HasOverriddenAttributes() const {
for (const auto& pair : constraints_) {
if (pair.second->HasOverriddenAttributes())
return true;
}
return false;
}
std::unique_ptr<base::Value> PropType::ToJson(bool full_schema,
chromeos::ErrorPtr* error) const {
if (!full_schema && !HasOverriddenAttributes()) {
if (based_on_schema_)
return std::unique_ptr<base::Value>(new base::DictionaryValue);
return TypedValueToJson(GetTypeAsString(), error);
}
std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue);
if (full_schema) {
// If we are asked for full_schema definition, then we need to output every
// parameter property, including the "type", and any constraints.
// So, we write the "type" only if asked for full schema.
// Otherwise we will be able to infer the parameter type based on
// the constraints and their types.
// That is, the following JSONs could possibly define a parameter:
// {'type':'integer'} -> explicit "integer" with no constraints
// {'minimum':10} -> no type specified, but since we have "minimum"
// and 10 is integer, than this is an integer
// parameter with min constraint.
// {'enum':[1,2,3]} -> integer with OneOf constraint.
// And so is this: [1,2,3] -> an array of ints assume it's an "int" enum.
dict->SetString(commands::attributes::kType, GetTypeAsString());
}
if (!full_schema && constraints_.size() == 1) {
// If we are not asked for full schema, and we have only one constraint
// which is OneOf, we short-circuit the whole thing and return just
// the array [1,2,3] instead of an object with "enum" property like:
// {'enum':[1,2,3]}
auto p = constraints_.find(ConstraintType::OneOf);
if (p != constraints_.end()) {
return p->second->ToJson(error);
}
}
for (const auto& pair : constraints_) {
if (!pair.second->AddToJsonDict(dict.get(), !full_schema, error))
return std::unique_ptr<base::Value>();
}
return std::unique_ptr<base::Value>(dict.release());
}
bool PropType::FromJson(const base::DictionaryValue* value,
const PropType* base_schema,
chromeos::ErrorPtr* error) {
if (base_schema && base_schema->GetType() != GetType()) {
chromeos::Error::AddToPrintf(error, errors::commands::kDomain,
errors::commands::kPropTypeChanged,
"Redefining a property of type %s as %s",
base_schema->GetTypeAsString().c_str(),
GetTypeAsString().c_str());
return false;
}
based_on_schema_ = (base_schema != nullptr);
constraints_.clear();
std::set<std::string> processed_keys{commands::attributes::kType};
if (!ObjectSchemaFromJson(value, base_schema, &processed_keys, error))
return false;
if (base_schema) {
for (const auto& pair : base_schema->GetConstraints()) {
std::shared_ptr<Constraint> inherited(pair.second->CloneAsInherited());
constraints_.insert(std::make_pair(pair.first, inherited));
}
}
if (!ConstraintsFromJson(value, &processed_keys, error))
return false;
// Now make sure there are no unexpected/unknown keys in the property schema
// definition object.
base::DictionaryValue::Iterator iter(*value);
while (!iter.IsAtEnd()) {
std::string key = iter.key();
if (processed_keys.find(key) == processed_keys.end()) {
chromeos::Error::AddToPrintf(error, errors::commands::kDomain,
errors::commands::kUnknownProperty,
"Unexpected property '%s'", key.c_str());
return false;
}
iter.Advance();
}
return true;
}
void PropType::AddConstraint(std::shared_ptr<Constraint> constraint) {
constraints_[constraint->GetType()] = constraint;
}
void PropType::RemoveConstraint(ConstraintType constraint_type) {
constraints_.erase(constraint_type);
}
const Constraint* PropType::GetConstraint(
ConstraintType constraint_type) const {
auto p = constraints_.find(constraint_type);
return p != constraints_.end() ? p->second.get() : nullptr;
}
Constraint* PropType::GetConstraint(ConstraintType constraint_type) {
auto p = constraints_.find(constraint_type);
return p != constraints_.end() ? p->second.get() : nullptr;
}
bool PropType::ValidateValue(const base::Value* value,
chromeos::ErrorPtr* error) const {
std::shared_ptr<PropValue> val = CreateValue();
CHECK(val) << "Failed to create value object";
return val->FromJson(value, error) && ValidateConstraints(*val, error);
}
bool PropType::ValidateValue(const chromeos::Any& value,
chromeos::ErrorPtr* error) const {
std::shared_ptr<PropValue> val = CreateValue(value, error);
return val && ValidateConstraints(*val, error);
}
bool PropType::ValidateConstraints(const PropValue& value,
chromeos::ErrorPtr* error) const {
for (const auto& pair : constraints_) {
if (!pair.second->Validate(value, error))
return false;
}
return true;
}
const PropType::TypeMap& PropType::GetTypeMap() {
static TypeMap map = {
{ValueType::Int, "integer"},
{ValueType::Double, "number"},
{ValueType::String, "string"},
{ValueType::Boolean, "boolean"},
{ValueType::Object, "object"},
};
return map;
}
std::string PropType::GetTypeStringFromType(ValueType type) {
for (const auto& pair : GetTypeMap()) {
if (pair.first == type)
return pair.second;
}
LOG(FATAL) << "Type map is missing a type";
return std::string();
}
bool PropType::GetTypeFromTypeString(const std::string& name, ValueType* type) {
for (const auto& pair : GetTypeMap()) {
if (pair.second == name) {
*type = pair.first;
return true;
}
}
return false;
}
std::unique_ptr<PropType> PropType::Create(ValueType type) {
PropType* prop = nullptr;
switch (type) {
case buffet::ValueType::Int:
prop = new IntPropType;
break;
case buffet::ValueType::Double:
prop = new DoublePropType;
break;
case buffet::ValueType::String:
prop = new StringPropType;
break;
case buffet::ValueType::Boolean:
prop = new BooleanPropType;
break;
case buffet::ValueType::Object:
prop = new ObjectPropType;
break;
}
return std::unique_ptr<PropType>(prop);
}
bool PropType::GenerateErrorValueTypeMismatch(chromeos::ErrorPtr* error) const {
chromeos::Error::AddToPrintf(error, errors::commands::kDomain,
errors::commands::kTypeMismatch,
"Unable to convert value to type '%s'",
GetTypeAsString().c_str());
return false;
}
template<typename T>
static std::shared_ptr<Constraint> LoadOneOfConstraint(
const base::DictionaryValue* value, const ObjectSchema* object_schema,
chromeos::ErrorPtr* error) {
const base::ListValue* list = nullptr;
if (!value->GetListWithoutPathExpansion(commands::attributes::kOneOf_Enum,
&list)) {
chromeos::Error::AddTo(error, errors::commands::kDomain,
errors::commands::kTypeMismatch,
"Expecting an array");
return std::shared_ptr<Constraint>();
}
std::vector<T> set;
set.reserve(list->GetSize());
for (const base::Value* item : *list) {
T val{};
if (!TypedValueFromJson(item, object_schema, &val, error))
return std::shared_ptr<Constraint>();
set.push_back(val);
}
InheritableAttribute<std::vector<T>> val(set, false);
return std::make_shared<ConstraintOneOf<T>>(val);
}
template<class ConstraintClass, typename T>
static std::shared_ptr<Constraint> LoadMinMaxConstraint(
const char* dict_key, const base::DictionaryValue* value,
const ObjectSchema* object_schema, chromeos::ErrorPtr* error) {
InheritableAttribute<T> limit;
const base::Value* src_val = nullptr;
CHECK(value->Get(dict_key, &src_val)) << "Unable to get min/max constraints";
if (!TypedValueFromJson(src_val, object_schema, &limit.value, error))
return std::shared_ptr<Constraint>();
limit.is_inherited = false;
return std::make_shared<ConstraintClass>(limit);
}
// PropTypeBase ----------------------------------------------------------------
template<class Derived, class Value, typename T>
bool PropTypeBase<Derived, Value, T>::ConstraintsFromJson(
const base::DictionaryValue* value, std::set<std::string>* processed_keys,
chromeos::ErrorPtr* error) {
if (!PropType::ConstraintsFromJson(value, processed_keys, error))
return false;
if (value->HasKey(commands::attributes::kOneOf_Enum)) {
auto constraint = LoadOneOfConstraint<T>(value, this->GetObjectSchemaPtr(),
error);
if (!constraint)
return false;
this->AddConstraint(constraint);
this->RemoveConstraint(ConstraintType::Min);
this->RemoveConstraint(ConstraintType::Max);
processed_keys->insert(commands::attributes::kOneOf_Enum);
}
return true;
}
// NumericPropTypeBase ---------------------------------------------------------
template<class Derived, class Value, typename T>
bool NumericPropTypeBase<Derived, Value, T>::ConstraintsFromJson(
const base::DictionaryValue* value, std::set<std::string>* processed_keys,
chromeos::ErrorPtr* error) {
if (!_Base::ConstraintsFromJson(value, processed_keys, error))
return false;
if (processed_keys->find(commands::attributes::kOneOf_Enum) ==
processed_keys->end()) {
// Process min/max constraints only if "enum" constraint wasn't already
// specified.
if (value->HasKey(commands::attributes::kNumeric_Min)) {
auto constraint = LoadMinMaxConstraint<ConstraintMin<T>, T>(
commands::attributes::kNumeric_Min, value, this->GetObjectSchemaPtr(),
error);
if (!constraint)
return false;
this->AddConstraint(constraint);
this->RemoveConstraint(ConstraintType::OneOf);
processed_keys->insert(commands::attributes::kNumeric_Min);
}
if (value->HasKey(commands::attributes::kNumeric_Max)) {
auto constraint = LoadMinMaxConstraint<ConstraintMax<T>, T>(
commands::attributes::kNumeric_Max, value, this->GetObjectSchemaPtr(),
error);
if (!constraint)
return false;
this->AddConstraint(constraint);
this->RemoveConstraint(ConstraintType::OneOf);
processed_keys->insert(commands::attributes::kNumeric_Max);
}
}
return true;
}
// StringPropType -------------------------------------------------------------
bool StringPropType::ConstraintsFromJson(
const base::DictionaryValue* value, std::set<std::string>* processed_keys,
chromeos::ErrorPtr* error) {
if (!_Base::ConstraintsFromJson(value, processed_keys, error))
return false;
if (processed_keys->find(commands::attributes::kOneOf_Enum) ==
processed_keys->end()) {
// Process min/max constraints only if "enum" constraint wasn't already
// specified.
if (value->HasKey(commands::attributes::kString_MinLength)) {
auto constraint = LoadMinMaxConstraint<ConstraintStringLengthMin, int>(
commands::attributes::kString_MinLength, value, GetObjectSchemaPtr(),
error);
if (!constraint)
return false;
AddConstraint(constraint);
RemoveConstraint(ConstraintType::OneOf);
processed_keys->insert(commands::attributes::kString_MinLength);
}
if (value->HasKey(commands::attributes::kString_MaxLength)) {
auto constraint = LoadMinMaxConstraint<ConstraintStringLengthMax, int>(
commands::attributes::kString_MaxLength, value, GetObjectSchemaPtr(),
error);
if (!constraint)
return false;
AddConstraint(constraint);
RemoveConstraint(ConstraintType::OneOf);
processed_keys->insert(commands::attributes::kString_MaxLength);
}
}
return true;
}
void StringPropType::AddLengthConstraint(int min_len, int max_len) {
InheritableAttribute<int> min_attr(min_len, false);
InheritableAttribute<int> max_attr(max_len, false);
AddConstraint(std::make_shared<ConstraintStringLengthMin>(min_attr));
AddConstraint(std::make_shared<ConstraintStringLengthMax>(max_attr));
}
int StringPropType::GetMinLength() const {
auto slc = static_cast<const ConstraintStringLength*>(
GetConstraint(ConstraintType::StringLengthMin));
return slc ? slc->limit_.value : 0;
}
int StringPropType::GetMaxLength() const {
auto slc = static_cast<const ConstraintStringLength*>(
GetConstraint(ConstraintType::StringLengthMax));
return slc ? slc->limit_.value : std::numeric_limits<int>::max();
}
// ObjectPropType -------------------------------------------------------------
ObjectPropType::ObjectPropType()
: object_schema_(std::make_shared<ObjectSchema>(), false) {}
bool ObjectPropType::HasOverriddenAttributes() const {
return PropType::HasOverriddenAttributes() ||
!object_schema_.is_inherited;
}
std::unique_ptr<base::Value> ObjectPropType::ToJson(
bool full_schema, chromeos::ErrorPtr* error) const {
std::unique_ptr<base::Value> value = PropType::ToJson(full_schema, error);
if (value) {
base::DictionaryValue* dict = nullptr;
CHECK(value->GetAsDictionary(&dict)) << "Expecting a JSON object";
if (!object_schema_.is_inherited || full_schema) {
auto object_schema = object_schema_.value->ToJson(full_schema, error);
if (!object_schema) {
value.reset();
return value;
}
dict->SetWithoutPathExpansion(commands::attributes::kObject_Properties,
object_schema.release());
}
}
return value;
}
bool ObjectPropType::ObjectSchemaFromJson(
const base::DictionaryValue* value, const PropType* base_schema,
std::set<std::string>* processed_keys, chromeos::ErrorPtr* error) {
if (!_Base::ObjectSchemaFromJson(value, base_schema, processed_keys, error))
return false;
using commands::attributes::kObject_Properties;
std::shared_ptr<const ObjectSchema> base_object_schema;
if (base_schema)
base_object_schema = base_schema->GetObject()->GetObjectSchema();
const base::DictionaryValue* props = nullptr;
if (value->GetDictionaryWithoutPathExpansion(kObject_Properties, &props)) {
processed_keys->insert(kObject_Properties);
auto object_schema = std::make_shared<ObjectSchema>();
if (!object_schema->FromJson(props, base_object_schema.get(), error)) {
chromeos::Error::AddTo(error, errors::commands::kDomain,
errors::commands::kInvalidObjectSchema,
"Error parsing object property schema");
return false;
}
object_schema_.value = object_schema;
object_schema_.is_inherited = false;
} else if (base_object_schema) {
object_schema_.value = base_object_schema;
object_schema_.is_inherited = true;
} else {
chromeos::Error::AddToPrintf(error, errors::commands::kDomain,
errors::commands::kInvalidObjectSchema,
"Object type definition must include the "
"object schema ('%s' field not found)",
kObject_Properties);
return false;
}
return true;
}
} // namespace buffet