| // 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 <utility> | 
 |  | 
 | #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->CreateDeepCopy()); | 
 |   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, std::move(prop_def)); | 
 |   } | 
 |   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 |