You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by ya...@apache.org on 2016/10/10 17:27:55 UTC
mesos git commit: Refactor parsing of resources.
Repository: mesos
Updated Branches:
refs/heads/master ba9adeebd -> d0c0430c4
Refactor parsing of resources.
Refactored `Resources::parse()` into 2 separate static functions:
1. Resources::fromJSON() to parse JSON representation of resources.
2. Resources::fromSimpleString() to parse text representation of
resources.
Note that these 2 new functions return a `Try<vector<Resource>>`, the
existing `Resources::parse()` calls them and converts the result to a
`Resources` object after validating each item in the vector. This
refactor is done to expose all successfully parsed resources for
further processing before semantic validation.
Review: https://reviews.apache.org/r/51999/
Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/d0c0430c
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/d0c0430c
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/d0c0430c
Branch: refs/heads/master
Commit: d0c0430c4b506e56d1da71d631d70b5678e634be
Parents: ba9adee
Author: Anindya Sinha <an...@apple.com>
Authored: Thu Oct 6 11:27:37 2016 -0700
Committer: Jiang Yan Xu <xu...@apple.com>
Committed: Mon Oct 10 10:27:44 2016 -0700
----------------------------------------------------------------------
include/mesos/resources.hpp | 69 +++++++++----
include/mesos/v1/resources.hpp | 69 +++++++++----
src/common/resources.cpp | 196 ++++++++++++++++++------------------
src/tests/resources_tests.cpp | 4 +-
src/v1/resources.cpp | 195 +++++++++++++++++------------------
5 files changed, 290 insertions(+), 243 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/include/mesos/resources.hpp
----------------------------------------------------------------------
diff --git a/include/mesos/resources.hpp b/include/mesos/resources.hpp
index ffbd466..f569c93 100644
--- a/include/mesos/resources.hpp
+++ b/include/mesos/resources.hpp
@@ -53,16 +53,6 @@
namespace mesos {
-// Forward declarations required for making
-// `convertJSON` a friend of `Resources`.
-class Resources;
-
-namespace internal {
- Try<Resources> convertJSON(
- const JSON::Array& resourcesJSON,
- const std::string& defaultRole);
-}
-
// NOTE: Resource objects stored in the class are always valid and
// kept combined if possible. It is the caller's responsibility to
// validate any Resource object or repeated Resource protobufs before
@@ -164,12 +154,11 @@ public:
/**
* Parses Resources from an input string.
*
- * Parses Resources from text in the form of a JSON array. If that fails,
- * parses text in the form "name(role):value;name:value;...". Any resource
- * that doesn't specify a role is assigned to the provided default role. See
- * the `Resource` protobuf definition for precise JSON formatting.
- *
- * Example JSON: [{"name":cpus","type":"SCALAR","scalar":{"value":8}}]
+ * Parses Resources from text in the form of a JSON array or as a simple
+ * string in the form of "name(role):value;name:value;...". i.e., this
+ * method calls `fromJSON()` or `fromSimpleString()` and validates the
+ * resulting `vector<Resource>` before converting it to a `Resources`
+ * object.
*
* @param text The input string.
* @param defaultRole The default role.
@@ -181,6 +170,50 @@ public:
const std::string& defaultRole = "*");
/**
+ * Parses an input JSON array into a vector of Resource objects.
+ *
+ * Parses into a vector of Resource objects from a JSON array. Any
+ * resource that doesn't specify a role is assigned to the provided
+ * default role. See the `Resource` protobuf definition for precise
+ * JSON formatting.
+ *
+ * Example JSON: [{"name":"cpus","type":"SCALAR","scalar":{"value":8}}]
+ *
+ * NOTE: The `Resource` objects in the result vector may not be valid
+ * semantically (i.e., they may not pass `Resources::validate()`). This
+ * is to allow additional handling of the parsing results in some cases.
+ *
+ * @param resourcesJSON The input JSON array.
+ * @param defaultRole The default role.
+ * @return A `Try` which contains the parsed vector of Resource objects
+ * if parsing was successful, or an Error otherwise.
+ */
+ static Try<std::vector<Resource>> fromJSON(
+ const JSON::Array& resourcesJSON,
+ const std::string& defaultRole = "*");
+
+ /**
+ * Parses an input text string into a vector of Resource objects.
+ *
+ * Parses into a vector of Resource objects from text. Any resource that
+ * doesn't specify a role is assigned to the provided default role.
+ *
+ * Example text: name(role):value;name:value;...
+ *
+ * NOTE: The `Resource` objects in the result vector may not be valid
+ * semantically (i.e., they may not pass `Resources::validate()`). This
+ * is to allow additional handling of the parsing results in some cases.
+ *
+ * @param text The input text string.
+ * @param defaultRole The default role.
+ * @return A `Try` which contains the parsed vector of Resource objects
+ * if parsing was successful, or an Error otherwise.
+ */
+ static Try<std::vector<Resource>> fromSimpleString(
+ const std::string& text,
+ const std::string& defaultRole = "*");
+
+ /**
* Validates a Resource object.
*
* Validates the given Resource object. Returns Error if it is not valid. A
@@ -487,10 +520,6 @@ public:
friend std::ostream& operator<<(
std::ostream& stream, const Resource_& resource_);
- friend Try<Resources> internal::convertJSON(
- const JSON::Array& resourcesJSON,
- const std::string& defaultRole);
-
private:
// Similar to 'contains(const Resource&)' but skips the validity
// check. This can be used to avoid the performance overhead of
http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/include/mesos/v1/resources.hpp
----------------------------------------------------------------------
diff --git a/include/mesos/v1/resources.hpp b/include/mesos/v1/resources.hpp
index 5ce038b..f60ab79 100644
--- a/include/mesos/v1/resources.hpp
+++ b/include/mesos/v1/resources.hpp
@@ -53,16 +53,6 @@
namespace mesos {
namespace v1 {
-// Forward declarations required for making
-// `convertJSON` a friend of `Resources`.
-class Resources;
-
-namespace internal {
- Try<Resources> convertJSON(
- const JSON::Array& resourcesJSON,
- const std::string& defaultRole);
-}
-
// NOTE: Resource objects stored in the class are always valid and
// kept combined if possible. It is the caller's responsibility to
// validate any Resource object or repeated Resource protobufs before
@@ -164,12 +154,11 @@ public:
/**
* Parses Resources from an input string.
*
- * Parses Resources from text in the form of a JSON array. If that fails,
- * parses text in the form "name(role):value;name:value;...". Any resource
- * that doesn't specify a role is assigned to the provided default role. See
- * the `Resource` protobuf definition for precise JSON formatting.
- *
- * Example JSON: [{"name":cpus","type":"SCALAR","scalar":{"value":8}}]
+ * Parses Resources from text in the form of a JSON array or as a simple
+ * string in the form of "name(role):value;name:value;...". i.e., this
+ * method calls `fromJSON()` or `fromSimpleString()` and validates the
+ * resulting `vector<Resource>` before converting it to a `Resources`
+ * object.
*
* @param text The input string.
* @param defaultRole The default role.
@@ -181,6 +170,50 @@ public:
const std::string& defaultRole = "*");
/**
+ * Parses an input JSON array into a vector of Resource objects.
+ *
+ * Parses into a vector of Resource objects from a JSON array. Any
+ * resource that doesn't specify a role is assigned to the provided
+ * default role. See the `Resource` protobuf definition for precise
+ * JSON formatting.
+ *
+ * Example JSON: [{"name":"cpus","type":"SCALAR","scalar":{"value":8}}]
+ *
+ * NOTE: The `Resource` objects in the result vector may not be valid
+ * semantically (i.e., they may not pass `Resources::validate()`). This
+ * is to allow additional handling of the parsing results in some cases.
+ *
+ * @param resourcesJSON The input JSON array.
+ * @param defaultRole The default role.
+ * @return A `Try` which contains the parsed vector of Resource objects
+ * if parsing was successful, or an Error otherwise.
+ */
+ static Try<std::vector<Resource>> fromJSON(
+ const JSON::Array& resourcesJSON,
+ const std::string& defaultRole = "*");
+
+ /**
+ * Parses an input text string into a vector of Resource objects.
+ *
+ * Parses into a vector of Resource objects from text. Any resource that
+ * doesn't specify a role is assigned to the provided default role.
+ *
+ * Example text: name(role):value;name:value;...
+ *
+ * NOTE: The `Resource` objects in the result vector may not be valid
+ * semantically (i.e., they may not pass `Resources::validate()`). This
+ * is to allow additional handling of the parsing results in some cases.
+ *
+ * @param text The input text string.
+ * @param defaultRole The default role.
+ * @return A `Try` which contains the parsed vector of Resource objects
+ * if parsing was successful, or an Error otherwise.
+ */
+ static Try<std::vector<Resource>> fromSimpleString(
+ const std::string& text,
+ const std::string& defaultRole = "*");
+
+ /**
* Validates a Resource object.
*
* Validates the given Resource object. Returns Error if it is not valid. A
@@ -487,10 +520,6 @@ public:
friend std::ostream& operator<<(
std::ostream& stream, const Resource_& resource_);
- friend Try<Resources> internal::convertJSON(
- const JSON::Array& resourcesJSON,
- const std::string& defaultRole);
-
private:
// Similar to 'contains(const Resource&)' but skips the validity
// check. This can be used to avoid the performance overhead of
http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/src/common/resources.cpp
----------------------------------------------------------------------
diff --git a/src/common/resources.cpp b/src/common/resources.cpp
index 0774ff0..4bb9bef 100644
--- a/src/common/resources.cpp
+++ b/src/common/resources.cpp
@@ -430,65 +430,6 @@ static Option<Error> validateCommandLineResources(const Resources& resources)
return None();
}
-
-/**
- * Converts a JSON Array to a Resources object.
- *
- * Converts a JSON Array to a Resources object. This uses JSON to protobuf
- * conversion, so the Array should contain JSON Objects, each of which follows
- * the format of the Resource protobuf message. If no role is specified, the
- * provided default role will be assigned. `Resources::validate()` is used to
- * validate the input objects, and empty Resource objects will return an error.
- *
- * Example: [{"name":cpus","type":"SCALAR","scalar":{"value":8}}]
- *
- * @param resourcesJSON The input JSON Array.
- * @param defaultRole The default role.
- * @return A `Try` containing a Resources object if conversion was successful,
- * or an Error otherwise.
- */
-inline Try<Resources> convertJSON(
- const JSON::Array& resourcesJSON,
- const string& defaultRole)
-{
- // Convert the JSON Array into a protobuf message and use
- // that to construct a new Resources object.
- Try<RepeatedPtrField<Resource>> resourcesProtobuf =
- protobuf::parse<RepeatedPtrField<Resource>>(resourcesJSON);
-
- if (resourcesProtobuf.isError()) {
- return Error(
- "Some JSON resources were not formatted properly: "
- + resourcesProtobuf.error());
- }
-
- // TODO(greggomann): Refactor this `RepeatedPtrField<Resource>` to `Resources`
- // conversion if/when there is a factory function for Resources. Use of the
- // `Resources(RepeatedPtrField<Resource>)` constructor is avoided here because
- // it doesn't allow us to catch errors that occur during construction.
- // Note: the related JIRA ticket is MESOS-3852.
- Resources result;
-
- foreach (Resource& resource, resourcesProtobuf.get()) {
- // Set the default role if none was specified.
- if (!resource.has_role()) {
- resource.set_role(defaultRole);
- }
-
- // Validate the Resource and make sure it isn't empty.
- Option<Error> error = Resources::validate(resource);
- if (error.isSome()) {
- return error.get();
- } else if (Resources::isEmpty(resource)) {
- return Error("Some JSON resources were empty: " + stringify(resource));
- }
-
- result.add(resource);
- }
-
- return result;
-}
-
} // namespace internal {
@@ -584,64 +525,119 @@ Try<Resources> Resources::parse(
const string& text,
const string& defaultRole)
{
+ // Try to parse as a JSON Array. Otherwise, parse as a text string.
+ Try<JSON::Array> json = JSON::parse<JSON::Array>(text);
+
+ Try<vector<Resource>> resources = json.isSome() ?
+ Resources::fromJSON(json.get(), defaultRole) :
+ Resources::fromSimpleString(text, defaultRole);
+
+ if (resources.isError()) {
+ return Error(resources.error());
+ }
+
Resources result;
- // Try to parse as a JSON Array.
- Try<JSON::Array> resourcesJSON = JSON::parse<JSON::Array>(text);
- if (resourcesJSON.isSome()) {
- Try<Resources> resources =
- internal::convertJSON(resourcesJSON.get(), defaultRole);
- if (resources.isError()) {
- return resources;
+ // Validate individual Resource objects.
+ foreach(const Resource& resource, resources.get()) {
+ // If invalid, propgate error instead of skipping the resource.
+ Option<Error> error = Resources::validate(resource);
+ if (error.isSome()) {
+ return error.get();
}
- result = resources.get();
- } else {
- foreach (const string& token, strings::tokenize(text, ";")) {
- vector<string> pair = strings::tokenize(token, ":");
- if (pair.size() != 2) {
- return Error(
- "Bad value for resources, missing or extra ':' in " + token);
- }
+ result.add(resource);
+ }
- string name;
- string role;
- size_t openParen = pair[0].find("(");
- if (openParen == string::npos) {
- name = strings::trim(pair[0]);
- role = defaultRole;
- } else {
- size_t closeParen = pair[0].find(")");
- if (closeParen == string::npos || closeParen < openParen) {
- return Error(
- "Bad value for resources, mismatched parentheses in " + token);
- }
+ // TODO(jmlvanre): Move this up into `Containerizer::resources`.
+ Option<Error> error = internal::validateCommandLineResources(result);
+ if (error.isSome()) {
+ return error.get();
+ }
- name = strings::trim(pair[0].substr(0, openParen));
+ return result;
+}
- role = strings::trim(pair[0].substr(
- openParen + 1,
- closeParen - openParen - 1));
- }
- Try<Resource> resource = Resources::parse(name, pair[1], role);
- if (resource.isError()) {
- return Error(resource.error());
- }
+Try<vector<Resource>> Resources::fromJSON(
+ const JSON::Array& resourcesJSON,
+ const string& defaultRole)
+{
+ // Convert the JSON Array into a protobuf message and use
+ // that to construct a vector of Resource object.
+ Try<RepeatedPtrField<Resource>> resourcesProtobuf =
+ protobuf::parse<RepeatedPtrField<Resource>>(resourcesJSON);
- result += resource.get();
- }
+ if (resourcesProtobuf.isError()) {
+ return Error(
+ "Some JSON resources were not formatted properly: " +
+ resourcesProtobuf.error());
}
- Option<Error> error = internal::validateCommandLineResources(result);
- if (error.isSome()) {
- return error.get();
+ vector<Resource> result;
+
+ foreach (Resource& resource, resourcesProtobuf.get()) {
+ // Set the default role if none was specified.
+ if (!resource.has_role()) {
+ resource.set_role(defaultRole);
+ }
+
+ // We add the Resource object even if it is empty or invalid.
+ result.push_back(resource);
}
return result;
}
+Try<vector<Resource>> Resources::fromSimpleString(
+ const string& text,
+ const string& defaultRole)
+{
+ vector<Resource> resources;
+
+ foreach (const string& token, strings::tokenize(text, ";")) {
+ // TODO(anindya_sinha): Allow text based representation of resources
+ // to specify PATH or MOUNT type disks along with its root.
+ vector<string> pair = strings::tokenize(token, ":");
+ if (pair.size() != 2) {
+ return Error(
+ "Bad value for resources, missing or extra ':' in " + token);
+ }
+
+ string name;
+ string role;
+ size_t openParen = pair[0].find("(");
+ if (openParen == string::npos) {
+ name = strings::trim(pair[0]);
+ role = defaultRole;
+ } else {
+ size_t closeParen = pair[0].find(")");
+ if (closeParen == string::npos || closeParen < openParen) {
+ return Error(
+ "Bad value for resources, mismatched parentheses in " + token);
+ }
+
+ name = strings::trim(pair[0].substr(0, openParen));
+
+ role = strings::trim(pair[0].substr(
+ openParen + 1,
+ closeParen - openParen - 1));
+ }
+
+ Try<Resource> resource = Resources::parse(name, pair[1], role);
+ if (resource.isError()) {
+ return Error(resource.error());
+ }
+
+ // We add the Resource object even if it is empty or invalid.
+ resources.push_back(resource.get());
+ }
+
+ return resources;
+}
+
+
Option<Error> Resources::validate(const Resource& resource)
{
if (resource.name().empty()) {
http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/src/tests/resources_tests.cpp
----------------------------------------------------------------------
diff --git a/src/tests/resources_tests.cpp b/src/tests/resources_tests.cpp
index 1823f25..6a12783 100644
--- a/src/tests/resources_tests.cpp
+++ b/src/tests/resources_tests.cpp
@@ -627,14 +627,14 @@ TEST(ResourcesTest, ParsingFromJSONError)
EXPECT_ERROR(Resources::parse(jsonString));
- // Empty Resources.
+ // Negative Resources.
jsonString =
"["
" {"
" \"name\": \"panda_power\","
" \"type\": \"SCALAR\","
" \"scalar\": {"
- " \"value\": 0"
+ " \"value\": -1"
" }"
" },"
" {"
http://git-wip-us.apache.org/repos/asf/mesos/blob/d0c0430c/src/v1/resources.cpp
----------------------------------------------------------------------
diff --git a/src/v1/resources.cpp b/src/v1/resources.cpp
index 62a644e..46cc00f 100644
--- a/src/v1/resources.cpp
+++ b/src/v1/resources.cpp
@@ -432,65 +432,6 @@ static Option<Error> validateCommandLineResources(const Resources& resources)
return None();
}
-
-/**
- * Converts a JSON Array to a Resources object.
- *
- * Converts a JSON Array to a Resources object. This uses JSON to protobuf
- * conversion, so the Array should contain JSON Objects, each of which follows
- * the format of the Resource protobuf message. If no role is specified, the
- * provided default role will be assigned. `Resources::validate()` is used to
- * validate the input objects, and empty Resource objects will return an error.
- *
- * Example: [{"name":cpus","type":"SCALAR","scalar":{"value":8}}]
- *
- * @param resourcesJSON The input JSON Array.
- * @param defaultRole The default role.
- * @return A `Try` containing a Resources object if conversion was successful,
- * or an Error otherwise.
- */
-inline Try<Resources> convertJSON(
- const JSON::Array& resourcesJSON,
- const string& defaultRole)
-{
- // Convert the JSON Array into a protobuf message and use
- // that to construct a new Resources object.
- Try<RepeatedPtrField<Resource>> resourcesProtobuf =
- protobuf::parse<RepeatedPtrField<Resource>>(resourcesJSON);
-
- if (resourcesProtobuf.isError()) {
- return Error(
- "Some JSON resources were not formatted properly: "
- + resourcesProtobuf.error());
- }
-
- // TODO(greggomann): Refactor this `RepeatedPtrField<Resource>` to `Resources`
- // conversion if/when there is a factory function for Resources. Use of the
- // `Resources(RepeatedPtrField<Resource>)` constructor is avoided here because
- // it doesn't allow us to catch errors that occur during construction.
- // Note: the related JIRA ticket is MESOS-3852.
- Resources result;
-
- foreach (Resource& resource, resourcesProtobuf.get()) {
- // Set the default role if none was specified.
- if (!resource.has_role()) {
- resource.set_role(defaultRole);
- }
-
- // Validate the Resource and make sure it isn't empty.
- Option<Error> error = Resources::validate(resource);
- if (error.isSome()) {
- return error.get();
- } else if (Resources::isEmpty(resource)) {
- return Error("Some JSON resources were empty: " + stringify(resource));
- }
-
- result.add(resource);
- }
-
- return result;
-}
-
} // namespace internal {
@@ -586,53 +527,28 @@ Try<Resources> Resources::parse(
const string& text,
const string& defaultRole)
{
- Resources result;
-
- // Try to parse as a JSON Array.
- Try<JSON::Array> resourcesJSON = JSON::parse<JSON::Array>(text);
- if (resourcesJSON.isSome()) {
- Try<Resources> resources =
- internal::convertJSON(resourcesJSON.get(), defaultRole);
- if (resources.isError()) {
- return resources;
- }
-
- result = resources.get();
- } else {
- foreach (const string& token, strings::tokenize(text, ";")) {
- vector<string> pair = strings::tokenize(token, ":");
- if (pair.size() != 2) {
- return Error(
- "Bad value for resources, missing or extra ':' in " + token);
- }
-
- string name;
- string role;
- size_t openParen = pair[0].find("(");
- if (openParen == string::npos) {
- name = strings::trim(pair[0]);
- role = defaultRole;
- } else {
- size_t closeParen = pair[0].find(")");
- if (closeParen == string::npos || closeParen < openParen) {
- return Error(
- "Bad value for resources, mismatched parentheses in " + token);
- }
+ // Try to parse as a JSON Array. Otherwise, parse as a text string.
+ Try<JSON::Array> json = JSON::parse<JSON::Array>(text);
- name = strings::trim(pair[0].substr(0, openParen));
+ Try<vector<Resource>> resources = json.isSome() ?
+ Resources::fromJSON(json.get(), defaultRole) :
+ Resources::fromSimpleString(text, defaultRole);
- role = strings::trim(pair[0].substr(
- openParen + 1,
- closeParen - openParen - 1));
- }
+ if (resources.isError()) {
+ return Error(resources.error());
+ }
- Try<Resource> resource = Resources::parse(name, pair[1], role);
- if (resource.isError()) {
- return Error(resource.error());
- }
+ Resources result;
- result += resource.get();
+ // Validate individual Resource objects.
+ foreach(const Resource& resource, resources.get()) {
+ // If invalid, propgate error instead of skipping the resource.
+ Option<Error> error = Resources::validate(resource);
+ if (error.isSome()) {
+ return error.get();
}
+
+ result.add(resource);
}
// TODO(jmlvanre): Move this up into `Containerizer::resources`.
@@ -645,6 +561,83 @@ Try<Resources> Resources::parse(
}
+Try<vector<Resource>> Resources::fromJSON(
+ const JSON::Array& resourcesJSON,
+ const string& defaultRole)
+{
+ // Convert the JSON Array into a protobuf message and use
+ // that to construct a vector of Resource object.
+ Try<RepeatedPtrField<Resource>> resourcesProtobuf =
+ protobuf::parse<RepeatedPtrField<Resource>>(resourcesJSON);
+
+ if (resourcesProtobuf.isError()) {
+ return Error(
+ "Some JSON resources were not formatted properly: " +
+ resourcesProtobuf.error());
+ }
+
+ vector<Resource> result;
+
+ foreach (Resource& resource, resourcesProtobuf.get()) {
+ // Set the default role if none was specified.
+ if (!resource.has_role()) {
+ resource.set_role(defaultRole);
+ }
+
+ // We add the Resource object even if it is empty or invalid.
+ result.push_back(resource);
+ }
+
+ return result;
+}
+
+
+Try<vector<Resource>> Resources::fromSimpleString(
+ const string& text,
+ const string& defaultRole)
+{
+ vector<Resource> resources;
+
+ foreach (const string& token, strings::tokenize(text, ";")) {
+ vector<string> pair = strings::tokenize(token, ":");
+ if (pair.size() != 2) {
+ return Error(
+ "Bad value for resources, missing or extra ':' in " + token);
+ }
+
+ string name;
+ string role;
+ size_t openParen = pair[0].find("(");
+ if (openParen == string::npos) {
+ name = strings::trim(pair[0]);
+ role = defaultRole;
+ } else {
+ size_t closeParen = pair[0].find(")");
+ if (closeParen == string::npos || closeParen < openParen) {
+ return Error(
+ "Bad value for resources, mismatched parentheses in " + token);
+ }
+
+ name = strings::trim(pair[0].substr(0, openParen));
+
+ role = strings::trim(pair[0].substr(
+ openParen + 1,
+ closeParen - openParen - 1));
+ }
+
+ Try<Resource> resource = Resources::parse(name, pair[1], role);
+ if (resource.isError()) {
+ return Error(resource.error());
+ }
+
+ // We add the Resource object even if it is empty or invalid.
+ resources.push_back(resource.get());
+ }
+
+ return resources;
+}
+
+
Option<Error> Resources::validate(const Resource& resource)
{
if (resource.name().empty()) {