buffet: Add Array type support for Buffet command and state props

Added definition of ArrayPropType and ArrayPropValue, added
parsing code to ObjectSchema to parse the array object type as well
as type detection code to detect array types from 'items', 'enum',
'default' properties of object schema definition.

Added a bunch of unit tests to verify that array definitions can
be parsed, values of type arrays can be constructuted and value
validation code works with arrays.

BUG=brillo:107
TEST=`FEATURES=test emerge-link buffet`

Change-Id: I0f7bbed012792e0a49fa1b071bb56fee512825a9
Reviewed-on: https://chromium-review.googlesource.com/261616
Reviewed-by: Vitaly Buka <vitalybuka@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Trybot-Ready: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/commands/object_schema_unittest.cc b/buffet/commands/object_schema_unittest.cc
index f257a67..b0332d2 100644
--- a/buffet/commands/object_schema_unittest.cc
+++ b/buffet/commands/object_schema_unittest.cc
@@ -26,20 +26,25 @@
 namespace {
 
 template<typename T>
-std::vector<T> GetOneOfValues(const buffet::PropType* prop_type) {
+std::vector<T> GetArrayValues(const buffet::native_types::Array& arr) {
   std::vector<T> values;
-  auto one_of = static_cast<const buffet::ConstraintOneOf*>(
-      prop_type->GetConstraint(buffet::ConstraintType::OneOf));
-  if (!one_of)
-    return values;
-
-  values.reserve(one_of->set_.value.size());
-  for (const auto& prop_value : one_of->set_.value) {
+  values.reserve(arr.size());
+  for (const auto& prop_value : arr) {
     values.push_back(prop_value->GetValueAsAny().Get<T>());
   }
   return values;
 }
 
+template<typename T>
+std::vector<T> GetOneOfValues(const buffet::PropType* prop_type) {
+  auto one_of = static_cast<const buffet::ConstraintOneOf*>(
+      prop_type->GetConstraint(buffet::ConstraintType::OneOf));
+  if (!one_of)
+    return {};
+
+  return GetArrayValues<T>(one_of->set_.value);
+}
+
 }  // anonymous namespace
 
 TEST(CommandSchema, IntPropType_Empty) {
@@ -57,6 +62,7 @@
   EXPECT_EQ(nullptr, prop.GetDouble());
   EXPECT_EQ(nullptr, prop.GetString());
   EXPECT_EQ(nullptr, prop.GetObject());
+  EXPECT_EQ(nullptr, prop.GetArray());
 }
 
 TEST(CommandSchema, IntPropType_ToJson) {
@@ -182,6 +188,7 @@
   EXPECT_EQ(nullptr, prop.GetDouble());
   EXPECT_EQ(nullptr, prop.GetString());
   EXPECT_EQ(nullptr, prop.GetObject());
+  EXPECT_EQ(nullptr, prop.GetArray());
 }
 
 TEST(CommandSchema, BoolPropType_ToJson) {
@@ -274,6 +281,7 @@
   EXPECT_EQ(&prop, prop.GetDouble());
   EXPECT_EQ(nullptr, prop.GetString());
   EXPECT_EQ(nullptr, prop.GetObject());
+  EXPECT_EQ(nullptr, prop.GetArray());
 }
 
 TEST(CommandSchema, DoublePropType_ToJson) {
@@ -396,6 +404,7 @@
   EXPECT_EQ(nullptr, prop.GetDouble());
   EXPECT_EQ(&prop, prop.GetString());
   EXPECT_EQ(nullptr, prop.GetObject());
+  EXPECT_EQ(nullptr, prop.GetArray());
 }
 
 TEST(CommandSchema, StringPropType_ToJson) {
@@ -522,6 +531,7 @@
   EXPECT_EQ(nullptr, prop.GetDouble());
   EXPECT_EQ(nullptr, prop.GetString());
   EXPECT_EQ(&prop, prop.GetObject());
+  EXPECT_EQ(nullptr, prop.GetArray());
 }
 
 TEST(CommandSchema, ObjectPropType_ToJson) {
@@ -669,6 +679,162 @@
 
 ///////////////////////////////////////////////////////////////////////////////
 
+TEST(CommandSchema, ArrayPropType_Empty) {
+  buffet::ArrayPropType prop;
+  EXPECT_FALSE(prop.HasOverriddenAttributes());
+  EXPECT_FALSE(prop.IsBasedOnSchema());
+  EXPECT_EQ(nullptr, prop.GetDefaultValue());
+  EXPECT_EQ(nullptr, prop.GetItemTypePtr());
+  prop.SetItemType(buffet::PropType::Create(buffet::ValueType::Int));
+  EXPECT_TRUE(prop.HasOverriddenAttributes());
+  EXPECT_FALSE(prop.IsBasedOnSchema());
+  EXPECT_NE(nullptr, prop.GetItemTypePtr());
+}
+
+TEST(CommandSchema, ArrayPropType_Types) {
+  buffet::ArrayPropType prop;
+  EXPECT_EQ(nullptr, prop.GetInt());
+  EXPECT_EQ(nullptr, prop.GetBoolean());
+  EXPECT_EQ(nullptr, prop.GetDouble());
+  EXPECT_EQ(nullptr, prop.GetString());
+  EXPECT_EQ(nullptr, prop.GetObject());
+  EXPECT_EQ(&prop, prop.GetArray());
+}
+
+TEST(CommandSchema, ArrayPropType_ToJson) {
+  buffet::ArrayPropType prop;
+  prop.SetItemType(buffet::PropType::Create(buffet::ValueType::Int));
+  EXPECT_EQ("{'items':'integer'}",
+            ValueToString(prop.ToJson(false, nullptr).get()));
+  EXPECT_EQ("{'items':{'type':'integer'},'type':'array'}",
+            ValueToString(prop.ToJson(true, nullptr).get()));
+  EXPECT_FALSE(prop.IsBasedOnSchema());
+  buffet::ArrayPropType prop2;
+  prop2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr);
+  EXPECT_EQ("{}", ValueToString(prop2.ToJson(false, nullptr).get()));
+  EXPECT_TRUE(prop2.IsBasedOnSchema());
+  prop2.FromJson(CreateDictionaryValue("{'default':[1,2,3]}").get(),
+                 &prop, nullptr);
+  EXPECT_EQ("{'default':[1,2,3]}",
+            ValueToString(prop2.ToJson(false, nullptr).get()));
+  EXPECT_EQ("{'default':[1,2,3],'items':{'type':'integer'},'type':'array'}",
+            ValueToString(prop2.ToJson(true, nullptr).get()));
+}
+
+TEST(CommandSchema, ArrayPropType_FromJson) {
+  buffet::ArrayPropType prop;
+  EXPECT_TRUE(prop.FromJson(
+      CreateDictionaryValue("{'items':'integer'}").get(), nullptr, nullptr));
+  EXPECT_EQ(buffet::ValueType::Int, prop.GetItemTypePtr()->GetType());
+
+  buffet::ArrayPropType prop2;
+  ASSERT_TRUE(prop2.FromJson(CreateDictionaryValue(
+    "{'items':'string','default':['foo', 'bar', 'baz']}").get(), nullptr,
+      nullptr));
+  ASSERT_NE(nullptr, prop2.GetDefaultValue());
+  const buffet::ArrayValue* defval = prop2.GetDefaultValue()->GetArray();
+  ASSERT_NE(nullptr, defval);
+  EXPECT_EQ((std::vector<std::string>{"foo", "bar", "baz"}),
+            GetArrayValues<std::string>(defval->GetValue()));
+}
+
+TEST(CommandSchema, ArrayPropType_Validate) {
+  buffet::ArrayPropType prop;
+  prop.FromJson(CreateDictionaryValue(
+      "{'items':{'minimum':2.3, 'maximum':10.5}}").get(), nullptr,
+      nullptr);
+
+  chromeos::ErrorPtr error;
+  EXPECT_TRUE(prop.ValidateValue(CreateValue("[3,4,10.5]").get(), &error));
+  error.reset();
+
+  EXPECT_FALSE(prop.ValidateValue(CreateValue("[2]").get(), &error));
+  EXPECT_EQ("out_of_range", error->GetCode());
+  EXPECT_EQ("Value 2 is out of range. It must not be less than 2.3",
+            error->GetMessage());
+  error.reset();
+
+  EXPECT_FALSE(prop.ValidateValue(CreateValue("[4, 5, 20]").get(), &error));
+  EXPECT_EQ("out_of_range", error->GetCode());
+  EXPECT_EQ("Value 20 is out of range. It must not be greater than 10.5",
+            error->GetMessage());
+  error.reset();
+}
+
+TEST(CommandSchema, ArrayPropType_Validate_Enum) {
+  buffet::ArrayPropType prop;
+  prop.FromJson(CreateDictionaryValue(
+      "{'items':'integer', 'enum':[[1], [2,3], [4,5,6]]}").get(), nullptr,
+      nullptr);
+
+  chromeos::ErrorPtr error;
+  EXPECT_TRUE(prop.ValidateValue(CreateValue("[2,3]").get(), &error));
+  error.reset();
+
+  EXPECT_FALSE(prop.ValidateValue(CreateValue("[2]").get(), &error));
+  EXPECT_EQ("out_of_range", error->GetCode());
+  EXPECT_EQ("Value [2] is invalid. Expected one of [[1],[2,3],[4,5,6]]",
+            error->GetMessage());
+  error.reset();
+
+  EXPECT_FALSE(prop.ValidateValue(CreateValue("[2,3,4]").get(), &error));
+  EXPECT_EQ("out_of_range", error->GetCode());
+  error.reset();
+}
+
+TEST(CommandSchema, ArrayPropType_CreateValue) {
+  buffet::ArrayPropType prop;
+  ASSERT_TRUE(prop.FromJson(CreateDictionaryValue(
+      "{'items':{'properties':{'width':'integer','height':'integer'}}}").get(),
+      nullptr, nullptr));
+
+  chromeos::ErrorPtr error;
+  buffet::native_types::Array arr;
+
+  auto val = prop.CreateValue(arr, &error);
+  ASSERT_NE(nullptr, val.get());
+  EXPECT_EQ(nullptr, error.get());
+  EXPECT_EQ(arr, val->GetValueAsAny().Get<buffet::native_types::Array>());
+  EXPECT_EQ("[]", ValueToString(val->ToJson(nullptr).get()));
+
+  buffet::IntPropType int_type;
+  buffet::ObjectPropType obj_type;
+  ASSERT_TRUE(obj_type.FromJson(CreateDictionaryValue(
+      "{'properties':{'width':'integer','height':'integer'}}").get(),
+      nullptr, nullptr));
+  arr.push_back(obj_type.CreateValue(buffet::native_types::Object{
+    {"width", int_type.CreateValue(10, nullptr)},
+    {"height", int_type.CreateValue(20, nullptr)},
+  }, nullptr));
+  arr.push_back(obj_type.CreateValue(buffet::native_types::Object{
+    {"width", int_type.CreateValue(17, nullptr)},
+    {"height", int_type.CreateValue(18, nullptr)},
+  }, nullptr));
+
+  val = prop.CreateValue(arr, &error);
+  ASSERT_NE(nullptr, val.get());
+  EXPECT_EQ(nullptr, error.get());
+  EXPECT_EQ(arr, val->GetValueAsAny().Get<buffet::native_types::Array>());
+  EXPECT_EQ("[{'height':20,'width':10},{'height':18,'width':17}]",
+            ValueToString(val->ToJson(nullptr).get()));
+
+  val = prop.CreateValue("blah", &error);
+  EXPECT_EQ(nullptr, val.get());
+  ASSERT_NE(nullptr, error.get());
+  EXPECT_EQ(buffet::errors::commands::kTypeMismatch, error->GetCode());
+}
+
+TEST(CommandSchema, ArrayPropType_NestedArrays_NotSupported) {
+  buffet::ArrayPropType prop;
+  chromeos::ErrorPtr error;
+  EXPECT_FALSE(prop.FromJson(CreateDictionaryValue(
+      "{'items':{'items':'integer'}}").get(), nullptr, &error));
+  EXPECT_EQ(buffet::errors::commands::kInvalidObjectSchema, error->GetCode());
+  error.reset();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
 TEST(CommandSchema, ObjectSchema_FromJson_Shorthand_TypeName) {
   buffet::ObjectSchema schema;
   const char* schema_str = "{"
@@ -703,17 +869,23 @@
   const char* schema_str = "{"
       "'param1':{'type':'integer'},"
       "'param2':{'type':'number'},"
-      "'param3':{'type':'string'}"
+      "'param3':{'type':'string'},"
+      "'param4':{'type':'array', 'items':'integer'},"
+      "'param5':{'type':'object', 'properties':{'p1':'integer'}}"
       "}";
   EXPECT_TRUE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
                               nullptr));
   EXPECT_EQ(buffet::ValueType::Int, schema.GetProp("param1")->GetType());
   EXPECT_EQ(buffet::ValueType::Double, schema.GetProp("param2")->GetType());
   EXPECT_EQ(buffet::ValueType::String, schema.GetProp("param3")->GetType());
+  EXPECT_EQ(buffet::ValueType::Array, schema.GetProp("param4")->GetType());
+  EXPECT_EQ(buffet::ValueType::Object, schema.GetProp("param5")->GetType());
   EXPECT_EQ("integer", schema.GetProp("param1")->GetTypeAsString());
   EXPECT_EQ("number", schema.GetProp("param2")->GetTypeAsString());
   EXPECT_EQ("string", schema.GetProp("param3")->GetTypeAsString());
-  EXPECT_EQ(nullptr, schema.GetProp("param4"));
+  EXPECT_EQ("array", schema.GetProp("param4")->GetTypeAsString());
+  EXPECT_EQ("object", schema.GetProp("param5")->GetTypeAsString());
+  EXPECT_EQ(nullptr, schema.GetProp("param77"));
 
   int min_int = (std::numeric_limits<int>::min)();
   int max_int = (std::numeric_limits<int>::max)();
@@ -745,7 +917,9 @@
       "'param13':{'default':13.5},"
       "'param14':{'default':true},"
       "'param15':{'default':false},"
-      "'param16':{'default':'foobar'}"
+      "'param16':{'default':'foobar'},"
+      "'param17':{'default':[1,2,3]},"
+      "'param18':{'items':'number', 'default':[]}"
       "}";
   EXPECT_TRUE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
                               nullptr));
@@ -765,6 +939,14 @@
   EXPECT_EQ("boolean", schema.GetProp("param14")->GetTypeAsString());
   EXPECT_EQ("boolean", schema.GetProp("param15")->GetTypeAsString());
   EXPECT_EQ("string", schema.GetProp("param16")->GetTypeAsString());
+  EXPECT_EQ("array", schema.GetProp("param17")->GetTypeAsString());
+  auto prop17 = schema.GetProp("param17");
+  EXPECT_EQ("integer",
+            prop17->GetArray()->GetItemTypePtr()->GetTypeAsString());
+  EXPECT_EQ("array", schema.GetProp("param18")->GetTypeAsString());
+  auto prop18 = schema.GetProp("param18");
+  EXPECT_EQ("number",
+            prop18->GetArray()->GetItemTypePtr()->GetTypeAsString());
 
   int min_int = (std::numeric_limits<int>::min)();
   int max_int = (std::numeric_limits<int>::max)();
@@ -804,6 +986,11 @@
   EXPECT_FALSE(val->GetBoolean()->GetValue());
   val = schema.GetProp("param16")->GetDefaultValue();
   EXPECT_EQ("foobar", val->GetString()->GetValue());
+  val = schema.GetProp("param17")->GetDefaultValue();
+  EXPECT_EQ((std::vector<int>{1, 2, 3}),
+            GetArrayValues<int>(val->GetArray()->GetValue()));
+  val = schema.GetProp("param18")->GetDefaultValue();
+  EXPECT_TRUE(val->GetArray()->GetValue().empty());
 }
 
 TEST(CommandSchema, ObjectSchema_FromJson_Shorthand_TypeDeduction_Array) {
@@ -818,8 +1005,11 @@
       "'param7' :{'type':'integer', 'enum':[1,2,3]},"
       "'param8' :{'type':'number',  'enum':[1,2,3]},"
       "'param9' :{'type':'number',  'enum':[]},"
-      "'param10':{'type':'integer', 'enum':[]}"
-      "}";
+      "'param10':{'type':'integer', 'enum':[]},"
+      "'param11':[[0,1],[2,3]],"
+      "'param12':[['foo','bar']],"
+      "'param13':{'enum':[['id0', 'id1']]}"
+     "}";
   EXPECT_TRUE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
                               nullptr));
   EXPECT_EQ("integer", schema.GetProp("param1")->GetTypeAsString());
@@ -833,6 +1023,21 @@
   EXPECT_EQ("number", schema.GetProp("param9")->GetTypeAsString());
   EXPECT_EQ("integer", schema.GetProp("param10")->GetTypeAsString());
 
+  auto prop_type11 = schema.GetProp("param11");
+  EXPECT_EQ("array", prop_type11->GetTypeAsString());
+  EXPECT_EQ("integer",
+            prop_type11->GetArray()->GetItemTypePtr()->GetTypeAsString());
+
+  auto prop_type12 = schema.GetProp("param12");
+  EXPECT_EQ("array", prop_type12->GetTypeAsString());
+  EXPECT_EQ("string",
+            prop_type12->GetArray()->GetItemTypePtr()->GetTypeAsString());
+
+  auto prop_type13 = schema.GetProp("param13");
+  EXPECT_EQ("array", prop_type12->GetTypeAsString());
+  EXPECT_EQ("string",
+            prop_type13->GetArray()->GetItemTypePtr()->GetTypeAsString());
+
   EXPECT_EQ((std::vector<int>{0, 1, 2, 3}),
             GetOneOfValues<int>(schema.GetProp("param1")));
   EXPECT_EQ((std::vector<double>{0.0, 1.1, 2.2}),
@@ -985,7 +1190,8 @@
       "'param3':{'default':3.3},"
       "'param4':{'default':'four'},"
       "'param5':{'default':{'x':5,'y':6},"
-                "'properties':{'x':'integer','y':'integer'}}"
+                "'properties':{'x':'integer','y':'integer'}},"
+      "'param6':{'default':[1,2,3]}"
       "}}";
   ASSERT_TRUE(prop.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
                             nullptr));
@@ -1001,6 +1207,8 @@
   buffet::native_types::Object param5 = obj["param5"]->GetObject()->GetValue();
   EXPECT_EQ(5, param5["x"]->GetInt()->GetValue());
   EXPECT_EQ(6, param5["y"]->GetInt()->GetValue());
+  buffet::native_types::Array param6 = obj["param6"]->GetArray()->GetValue();
+  EXPECT_EQ((std::vector<int>{1, 2, 3}), GetArrayValues<int>(param6));
 
   // Specify some.
   value = prop.CreateValue();
@@ -1018,6 +1226,8 @@
   param5 = obj["param5"]->GetObject()->GetValue();
   EXPECT_EQ(-5, param5["x"]->GetInt()->GetValue());
   EXPECT_EQ(-6, param5["y"]->GetInt()->GetValue());
+  param6 = obj["param6"]->GetArray()->GetValue();
+  EXPECT_EQ((std::vector<int>{1, 2, 3}), GetArrayValues<int>(param6));
 
   // Specify all.
   value = prop.CreateValue();
@@ -1026,7 +1236,8 @@
       "'param2':22,"
       "'param3':333.3,"
       "'param4':'FOUR',"
-      "'param5':{'x':-55,'y':66}"
+      "'param5':{'x':-55,'y':66},"
+      "'param6':[-1, 0]"
       "}";
   ASSERT_TRUE(value->FromJson(CreateDictionaryValue(val_json).get(), nullptr));
   obj = value->GetObject()->GetValue();
@@ -1037,6 +1248,8 @@
   param5 = obj["param5"]->GetObject()->GetValue();
   EXPECT_EQ(-55, param5["x"]->GetInt()->GetValue());
   EXPECT_EQ(66, param5["y"]->GetInt()->GetValue());
+  param6 = obj["param6"]->GetArray()->GetValue();
+  EXPECT_EQ((std::vector<int>{-1, 0}), GetArrayValues<int>(param6));
 }
 
 TEST(CommandSchema, ObjectSchema_FromJson_BaseSchema_Failures) {
@@ -1105,4 +1318,52 @@
                                &error));
   EXPECT_EQ("out_of_range", error->GetFirstError()->GetCode());
   error.reset();
+
+  schema_str = "{"
+      "'param1':[[1,2.3]]"
+      "}";
+  EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
+                               &error));
+  EXPECT_EQ("type_mismatch", error->GetFirstError()->GetCode());
+  error.reset();
+
+  schema_str = "{"
+      "'param1':[[1,2],[3,4],['blah']]"
+      "}";
+  EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
+                               &error));
+  EXPECT_EQ("type_mismatch", error->GetFirstError()->GetCode());
+  error.reset();
+
+  schema_str = "{"
+      "'param1':{'default':[]}"
+      "}";
+  EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
+                               &error));
+  EXPECT_EQ("no_type_info", error->GetFirstError()->GetCode());
+  error.reset();
+
+  schema_str = "{"
+      "'param1':[[[1]],[[2]]]"
+      "}";
+  EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
+                               &error));
+  EXPECT_EQ("no_type_info", error->GetFirstError()->GetCode());
+  error.reset();
+
+  schema_str = "{"
+      "'param1':{'enum':[[['foo']]]}"
+      "}";
+  EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
+                               &error));
+  EXPECT_EQ("no_type_info", error->GetFirstError()->GetCode());
+  error.reset();
+
+  schema_str = "{"
+      "'param1':{'default':[[1],[2]]}"
+      "}";
+  EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr,
+                               &error));
+  EXPECT_EQ("no_type_info", error->GetFirstError()->GetCode());
+  error.reset();
 }