Resetting weave-release-1.1 to 03cd192 BUG=25936428 TEST=None Change-Id: Id60979589bc8c2c15fc5e0126dc836c3ed8b5f8b Reviewed-on: https://weave-review.googlesource.com/1683 Reviewed-by: Ben Henry <benhenry@google.com>
diff --git a/.gitignore b/.gitignore index 3ee16a1..4011af4 100644 --- a/.gitignore +++ b/.gitignore
@@ -1,7 +1,4 @@ -*.target.mk *~ /out/ /third_party/include /third_party/lib -gomacc.lock -Makefile
diff --git a/README b/README index 9b357a0..bf9b70b 100644 --- a/README +++ b/README
@@ -71,6 +71,7 @@ libtool gyp libexpat1-dev + ninja-build For tests: @@ -89,18 +90,18 @@ --------- Everywhere below Debug can be replaced with Release. -Generate build files: +Generate ninja build files: gyp -I libweave_common.gypi --toplevel-dir=. --depth=. \ - -f make libweave_standalone.gyp + -f ninja libweave_standalone.gyp Build library with tests: - make + ninja -C out/Debug Build library only: - make libweave + ninja -C out/Debug libweave Testing ------- @@ -129,3 +130,7 @@ repo upload . Go to the url from the output of "repo upload" and add reviewers. + +Known Issues +------------ +* No big-endian support. Pairing fails on big-endian hardware.
diff --git a/examples/build.sh b/examples/build.sh index 262c56b..e4c412d 100755 --- a/examples/build.sh +++ b/examples/build.sh
@@ -8,7 +8,7 @@ cd $ROOT_DIR -gyp -Ilibweave_common.gypi --toplevel-dir=. --depth=. -f make $DIR/daemon/examples.gyp +gyp -Ilibweave_common.gypi --toplevel-dir=. --depth=. -f ninja $DIR/daemon/examples.gyp if [ -z "$BUILD_CONFIG" ]; then export BUILD_CONFIG=Debug @@ -20,7 +20,7 @@ fi export CORES=`cat /proc/cpuinfo | grep processor | wc -l` -BUILDTYPE=$BUILD_CONFIG make -j $CORES $BUILD_TARGET || exit 1 +ninja -j $CORES -C out/${BUILD_CONFIG} $BUILD_TARGET || exit 1 if [[ $BUILD_TARGET == *"libweave_testrunner"* ]]; then out/${BUILD_CONFIG}/libweave_testrunner --gtest_break_on_failure || exit 1
diff --git a/examples/daemon/ledflasher/ledflasher.cc b/examples/daemon/ledflasher/ledflasher.cc index 9e4a9e1..38314f5 100644 --- a/examples/daemon/ledflasher/ledflasher.cc +++ b/examples/daemon/ledflasher/ledflasher.cc
@@ -25,7 +25,7 @@ device_ = device; device->AddStateDefinitionsFromJson(R"({ - "_ledflasher": {"_leds": {"type": "array", "items": {"type": "boolean"}}} + "_ledflasher": {"_leds": {"items": "boolean"}} })"); device->SetStatePropertiesFromJson(R"({ @@ -35,18 +35,16 @@ device->AddCommandDefinitionsFromJson(R"({ "_ledflasher": { - "_set":{ - "minimalRole": "user", - "parameters": { - "_led": {"type": "integer", "minimum": 1, "maximum": 3}, - "_on": {"type": "boolean"} - } - }, - "_toggle":{ - "minimalRole": "user", - "parameters": { - "_led": {"type": "integer", "minimum": 1, "maximum": 3} - } + "_set":{ + "parameters": { + "_led": {"minimum": 1, "maximum": 3}, + "_on": "boolean" + } + }, + "_toggle":{ + "parameters": { + "_led": {"minimum": 1, "maximum": 3} + } } } })"); @@ -66,10 +64,9 @@ return; LOG(INFO) << "received command: " << cmd->GetName(); int32_t led_index = 0; - const auto& params = cmd->GetParameters(); bool cmd_value = false; - if (params.GetInteger("_led", &led_index) && - params.GetBoolean("_on", &cmd_value)) { + if (cmd->GetParameters()->GetInteger("_led", &led_index) && + cmd->GetParameters()->GetBoolean("_on", &cmd_value)) { // Display this command in terminal LOG(INFO) << cmd->GetName() << " _led: " << led_index << ", _on: " << (cmd_value ? "true" : "false"); @@ -96,9 +93,8 @@ if (!cmd) return; LOG(INFO) << "received command: " << cmd->GetName(); - const auto& params = cmd->GetParameters(); int32_t led_index = 0; - if (params.GetInteger("_led", &led_index)) { + if (cmd->GetParameters()->GetInteger("_led", &led_index)) { LOG(INFO) << cmd->GetName() << " _led: " << led_index; led_index--; led_status_[led_index] = ~led_status_[led_index];
diff --git a/examples/daemon/light/light.cc b/examples/daemon/light/light.cc index 90680d2..d54de93 100644 --- a/examples/daemon/light/light.cc +++ b/examples/daemon/light/light.cc
@@ -18,31 +18,31 @@ device_ = device; device->AddStateDefinitionsFromJson(R"({ - "onOff": {"state": {"type": "string", "enum": ["on", "standby"]}}, - "brightness": {"brightness": {"type": "integer"}}, + "onOff": {"state": ["on", "standby"]}, + "brightness": {"brightness": "integer"}, "colorXY": { "colorSetting": { "properties": { - "colorX": {"type": "number", "minimum": 0.0, "maximum": 1.0}, - "colorY": {"type": "number", "minimum": 0.0, "maximum": 1.0} + "colorX": {"minimum": 0.0, "maximum": 1.0}, + "colorY": {"minimum": 0.0, "maximum": 1.0} } }, "colorCapRed": { "properties": { - "colorX": {"type": "number", "minimum": 0.0, "maximum": 1.0}, - "colorY": {"type": "number", "minimum": 0.0, "maximum": 1.0} + "colorX": {"minimum": 0.0, "maximum": 1.0}, + "colorY": {"minimum": 0.0, "maximum": 1.0} } }, "colorCapGreen": { "properties": { - "colorX": {"type": "number", "minimum": 0.0, "maximum": 1.0}, - "colorY": {"type": "number", "minimum": 0.0, "maximum": 1.0} + "colorX": {"minimum": 0.0, "maximum": 1.0}, + "colorY": {"minimum": 0.0, "maximum": 1.0} } }, "colorCapBlue": { "properties": { - "colorX": {"type": "number", "minimum": 0.0, "maximum": 1.0}, - "colorY": {"type": "number", "minimum": 0.0, "maximum": 1.0} + "colorX": {"minimum": 0.0, "maximum": 1.0}, + "colorY": {"minimum": 0.0, "maximum": 1.0} } } } @@ -62,23 +62,21 @@ device->AddCommandDefinitionsFromJson(R"({ "onOff": { - "setConfig":{ - "minimalRole": "user", - "parameters": { - "state": {"type": "string", "enum": ["on", "standby"]} - } - } - }, - "brightness": { - "setConfig":{ - "minimalRole": "user", - "parameters": { - "brightness": { - "type": "integer", - "minimum": 0, - "maximum": 100 - } - } + "setConfig":{ + "parameters": { + "state": ["on", "standby"] + } + } + }, + "brightness": { + "setConfig":{ + "parameters": { + "brightness": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + } } }, "_colorXY": { @@ -90,16 +88,15 @@ "properties": { "_colorX": { "type": "number", - "minimum": 0.0, - "maximum": 1.0 + "minimum": 0, + "maximum": 1 }, "_colorY": { "type": "number", - "minimum": 0.0, - "maximum": 1.0 + "minimum": 0, + "maximum": 1 } - }, - "additionalProperties": false + } } } } @@ -122,9 +119,8 @@ if (!cmd) return; LOG(INFO) << "received command: " << cmd->GetName(); - const auto& params = cmd->GetParameters(); int32_t brightness_value = 0; - if (params.GetInteger("brightness", &brightness_value)) { + if (cmd->GetParameters()->GetInteger("brightness", &brightness_value)) { // Display this command in terminal. LOG(INFO) << cmd->GetName() << " brightness: " << brightness_value; @@ -146,9 +142,8 @@ if (!cmd) return; LOG(INFO) << "received command: " << cmd->GetName(); - const auto& params = cmd->GetParameters(); std::string requested_state; - if (params.GetString("state", &requested_state)) { + if (cmd->GetParameters()->GetString("state", &requested_state)) { LOG(INFO) << cmd->GetName() << " state: " << requested_state; bool new_light_status = requested_state == "on"; @@ -172,9 +167,9 @@ if (!cmd) return; LOG(INFO) << "received command: " << cmd->GetName(); - const auto& params = cmd->GetParameters(); - const base::DictionaryValue* colorXY = nullptr; - if (params.GetDictionary("_colorSetting", &colorXY)) { + auto params = cmd->GetParameters(); + base::DictionaryValue* colorXY = nullptr; + if (params->GetDictionary("_colorSetting", &colorXY)) { bool updateState = false; double X = 0.0; double Y = 0.0;
diff --git a/examples/daemon/lock/lock.cc b/examples/daemon/lock/lock.cc index 10bea00..3014fb1 100644 --- a/examples/daemon/lock/lock.cc +++ b/examples/daemon/lock/lock.cc
@@ -35,12 +35,8 @@ device->AddStateDefinitionsFromJson(R"({ "lock": { - "lockedState": { - "type": "string", - "enum": ["locked", "unlocked", "partiallyLocked"] - }, - "isLockingSupported": {"type": "boolean"} - } + "lockedState": ["locked", "unlocked", "partiallyLocked"], + "isLockingSupported": "boolean"} })"); device->SetStatePropertiesFromJson(R"({ @@ -52,14 +48,13 @@ nullptr); device->AddCommandDefinitionsFromJson(R"({ - "lock": { - "setConfig":{ - "minimalRole": "user", - "parameters": { - "lockedState": {"type": "string", "enum":["locked", "unlocked"]} + "lock": { + "setConfig":{ + "parameters": { + "lockedState": ["locked", "unlocked"] + } } } - } })"); device->AddCommandHandler("lock.setConfig", base::Bind(&LockHandler::OnLockSetConfig, @@ -72,9 +67,8 @@ if (!cmd) return; LOG(INFO) << "received command: " << cmd->GetName(); - const auto& params = cmd->GetParameters(); std::string requested_state; - if (params.GetString("lockedState", &requested_state)) { + if (cmd->GetParameters()->GetString("lockedState", &requested_state)) { LOG(INFO) << cmd->GetName() << " state: " << requested_state; weave::lockstate::LockState new_lock_status;
diff --git a/examples/daemon/sample/sample.cc b/examples/daemon/sample/sample.cc index 811e9fb..905a977 100644 --- a/examples/daemon/sample/sample.cc +++ b/examples/daemon/sample/sample.cc
@@ -27,23 +27,27 @@ "_hello": { "minimalRole": "user", "parameters": { - "_name": {"type": "string"} - } + "_name": "string" + }, + "results": { "_reply": "string" } }, "_ping": { - "minimalRole": "user" + "minimalRole": "user", + "results": {} }, "_countdown": { "minimalRole": "user", "parameters": { - "_seconds": {"type": "integer", "minimum": 1, "maximum": 25} - } + "_seconds": {"minimum": 1, "maximum": 25} + }, + "progress": { "_seconds_left": "integer"}, + "results": {} } } })"); device->AddStateDefinitionsFromJson(R"({ - "_sample": {"_ping_count": {"type": "integer"}} + "_sample": {"_ping_count":"integer"} })"); device->SetStatePropertiesFromJson(R"({ @@ -69,9 +73,8 @@ return; LOG(INFO) << "received command: " << cmd->GetName(); - const auto& params = cmd->GetParameters(); std::string name; - if (!params.GetString("_name", &name)) { + if (!cmd->GetParameters()->GetString("_name", &name)) { weave::ErrorPtr error; weave::Error::AddTo(&error, FROM_HERE, "example", "invalid_parameter_value", "Name is missing"); @@ -94,7 +97,7 @@ base::DictionaryValue state; state.SetInteger("_sample._ping_count", ++ping_count_); device_->SetStateProperties(state, nullptr); - LOG(INFO) << "New state: " << device_->GetState(); + LOG(INFO) << "New state: " << *device_->GetState(); base::DictionaryValue result; cmd->Complete(result, nullptr); @@ -108,9 +111,8 @@ return; LOG(INFO) << "received command: " << cmd->GetName(); - const auto& params = cmd->GetParameters(); int seconds; - if (!params.GetInteger("_seconds", &seconds)) + if (!cmd->GetParameters()->GetInteger("_seconds", &seconds)) seconds = 10; LOG(INFO) << "starting countdown"; @@ -123,9 +125,8 @@ return; if (seconds > 0) { - const auto& params = cmd->GetParameters(); std::string todo; - params.GetString("_todo", &todo); + cmd->GetParameters()->GetString("_todo", &todo); LOG(INFO) << "countdown tick: " << seconds << " seconds left"; base::DictionaryValue progress;
diff --git a/examples/daemon/speaker/speaker.cc b/examples/daemon/speaker/speaker.cc index cd7d62f..32591f9 100644 --- a/examples/daemon/speaker/speaker.cc +++ b/examples/daemon/speaker/speaker.cc
@@ -18,10 +18,10 @@ device_ = device; device->AddStateDefinitionsFromJson(R"({ - "onOff": {"state": {"type": "string", "enum": ["on", "standby"]}}, + "onOff": {"state": ["on", "standby"]}, "volume": { - "volume": {"type": "integer"}, - "isMuted": {"type": "boolean"} + "volume": "integer", + "isMuted": "boolean" } })"); @@ -36,24 +36,22 @@ device->AddCommandDefinitionsFromJson(R"({ "onOff": { - "setConfig":{ - "minimalRole": "user", - "parameters": { - "state": {"type": "string", "enum": ["on", "standby"]} - } - } - }, - "volume": { - "setConfig":{ - "minimalRole": "user", - "parameters": { - "volume": { - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "isMuted": {"type": "boolean"} - } + "setConfig":{ + "parameters": { + "state": ["on", "standby"] + } + } + }, + "volume": { + "setConfig":{ + "parameters": { + "volume": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "isMuted": "boolean" + } } } })"); @@ -72,10 +70,9 @@ return; LOG(INFO) << "received command: " << cmd->GetName(); - const auto& params = cmd->GetParameters(); // Handle volume parameter int32_t volume_value = 0; - if (params.GetInteger("volume", &volume_value)) { + if (cmd->GetParameters()->GetInteger("volume", &volume_value)) { // Display this command in terminal. LOG(INFO) << cmd->GetName() << " volume: " << volume_value; @@ -89,7 +86,7 @@ // Handle isMuted parameter bool isMuted_status = false; - if (params.GetBoolean("isMuted", &isMuted_status)) { + if (cmd->GetParameters()->GetBoolean("isMuted", &isMuted_status)) { // Display this command in terminal. LOG(INFO) << cmd->GetName() << " is " << (isMuted_status ? "muted" : "not muted"); @@ -111,9 +108,8 @@ if (!cmd) return; LOG(INFO) << "received command: " << cmd->GetName(); - const auto& params = cmd->GetParameters(); std::string requested_state; - if (params.GetString("state", &requested_state)) { + if (cmd->GetParameters()->GetString("state", &requested_state)) { LOG(INFO) << cmd->GetName() << " state: " << requested_state; bool new_speaker_status = requested_state == "on";
diff --git a/examples/prerequisites.sh b/examples/prerequisites.sh index 1b27806..7358b71 100755 --- a/examples/prerequisites.sh +++ b/examples/prerequisites.sh
@@ -20,6 +20,7 @@ libnl-route-3-dev \ libssl-dev \ libtool \ + ninja-build \ || exit 1 mkdir -p $ROOT_DIR/third_party/lib $ROOT_DIR/third_party/include 2> /dev/null
diff --git a/examples/provider/avahi_client.h b/examples/provider/avahi_client.h index 7d9b932..0ca28db 100644 --- a/examples/provider/avahi_client.h +++ b/examples/provider/avahi_client.h
@@ -34,7 +34,7 @@ std::unique_ptr<AvahiThreadedPoll, decltype(&avahi_threaded_poll_free)> thread_pool_{nullptr, &avahi_threaded_poll_free}; - std::unique_ptr< ::AvahiClient, decltype(&avahi_client_free)> client_{ + std::unique_ptr<::AvahiClient, decltype(&avahi_client_free)> client_{ nullptr, &avahi_client_free}; std::unique_ptr<AvahiEntryGroup, decltype(&avahi_entry_group_free)> group_{
diff --git a/examples/provider/event_http_client.cc b/examples/provider/event_http_client.cc index 1931547..03da97f 100644 --- a/examples/provider/event_http_client.cc +++ b/examples/provider/event_http_client.cc
@@ -91,7 +91,7 @@ const Headers& headers, const std::string& data, const SendRequestCallback& callback) { - evhttp_cmd_type method_id = EVHTTP_REQ_GET; + evhttp_cmd_type method_id; CHECK(weave::StringToEnum(weave::EnumToString(method), &method_id)); EventPtr<evhttp_uri> http_uri{evhttp_uri_parse(url.c_str())}; CHECK(http_uri);
diff --git a/examples/provider/event_http_server.cc b/examples/provider/event_http_server.cc index ae8bbec..e0ecea6 100644 --- a/examples/provider/event_http_server.cc +++ b/examples/provider/event_http_server.cc
@@ -164,14 +164,14 @@ void HttpServerImpl::AddHttpRequestHandler( const std::string& path, const RequestHandlerCallback& callback) { - handlers_.insert(std::make_pair(path, callback)); + handlers_.emplace(path, callback); evhttp_set_cb(httpd_.get(), path.c_str(), &ProcessRequestCallback, this); } void HttpServerImpl::AddHttpsRequestHandler( const std::string& path, const RequestHandlerCallback& callback) { - handlers_.insert(std::make_pair(path, callback)); + handlers_.emplace(path, callback); evhttp_set_cb(httpsd_.get(), path.c_str(), &ProcessRequestCallback, this); }
diff --git a/examples/provider/event_task_runner.cc b/examples/provider/event_task_runner.cc index 1d94612..c07e912 100644 --- a/examples/provider/event_task_runner.cc +++ b/examples/provider/event_task_runner.cc
@@ -34,8 +34,7 @@ flags |= (what & kClosed) ? EV_CLOSED : 0; event* ioevent = event_new(base_.get(), fd, flags, FdEventHandler, this); EventPtr<event> ioeventPtr{ioevent}; - fd_task_map_.insert( - std::make_pair(fd, std::make_pair(std::move(ioeventPtr), task))); + fd_task_map_.emplace(fd, std::make_pair(std::move(ioeventPtr), task)); event_add(ioevent, nullptr); }
diff --git a/include/weave/command.h b/include/weave/command.h index 0a7d545..59a9305 100644 --- a/include/weave/command.h +++ b/include/weave/command.h
@@ -40,13 +40,13 @@ virtual Command::Origin GetOrigin() const = 0; // Returns the command parameters. - virtual const base::DictionaryValue& GetParameters() const = 0; + virtual std::unique_ptr<base::DictionaryValue> GetParameters() const = 0; // Returns the command progress. - virtual const base::DictionaryValue& GetProgress() const = 0; + virtual std::unique_ptr<base::DictionaryValue> GetProgress() const = 0; // Returns the command results. - virtual const base::DictionaryValue& GetResults() const = 0; + virtual std::unique_ptr<base::DictionaryValue> GetResults() const = 0; // Returns the command error. virtual const Error* GetError() const = 0; @@ -79,7 +79,7 @@ virtual bool Cancel(ErrorPtr* error) = 0; protected: - virtual ~Command() {} + virtual ~Command() = default; }; } // namespace weave
diff --git a/include/weave/device.h b/include/weave/device.h index 5e86dfd..19012b5 100644 --- a/include/weave/device.h +++ b/include/weave/device.h
@@ -32,7 +32,7 @@ class Device { public: - virtual ~Device() {} + virtual ~Device() = default; // Returns reference the current settings. virtual const Settings& GetSettings() const = 0; @@ -96,7 +96,7 @@ // Returns value of the single property. // |name| is full property name, including package name. e.g. "base.network". - virtual const base::Value* GetStateProperty( + virtual std::unique_ptr<base::Value> GetStateProperty( const std::string& name) const = 0; // Sets value of the single property. @@ -106,7 +106,7 @@ ErrorPtr* error) = 0; // Returns aggregated state properties across all registered packages. - virtual const base::DictionaryValue& GetState() const = 0; + virtual std::unique_ptr<base::DictionaryValue> GetState() const = 0; // Returns current state of GCD connection. virtual GcdState GetGcdState() const = 0;
diff --git a/include/weave/provider/bluetooth.h b/include/weave/provider/bluetooth.h index 6a47e92..e8f3b3c 100644 --- a/include/weave/provider/bluetooth.h +++ b/include/weave/provider/bluetooth.h
@@ -14,7 +14,7 @@ // TODO(rginda): Add bluetooth interface methods here. protected: - virtual ~Bluetooth() {} + virtual ~Bluetooth() = default; }; } // namespace provider
diff --git a/include/weave/provider/config_store.h b/include/weave/provider/config_store.h index 1b7988f..53c1128 100644 --- a/include/weave/provider/config_store.h +++ b/include/weave/provider/config_store.h
@@ -75,7 +75,7 @@ virtual void SaveSettings(const std::string& settings) = 0; protected: - virtual ~ConfigStore() {} + virtual ~ConfigStore() = default; }; } // namespace provider
diff --git a/include/weave/provider/dns_service_discovery.h b/include/weave/provider/dns_service_discovery.h index 37bf84b..fa9d50e 100644 --- a/include/weave/provider/dns_service_discovery.h +++ b/include/weave/provider/dns_service_discovery.h
@@ -91,7 +91,7 @@ virtual void StopPublishing(const std::string& service_type) = 0; protected: - virtual ~DnsServiceDiscovery() {} + virtual ~DnsServiceDiscovery() = default; }; } // namespace provider
diff --git a/include/weave/provider/http_client.h b/include/weave/provider/http_client.h index bf01022..deb127a 100644 --- a/include/weave/provider/http_client.h +++ b/include/weave/provider/http_client.h
@@ -78,7 +78,7 @@ virtual std::string GetContentType() const = 0; virtual std::string GetData() const = 0; - virtual ~Response() {} + virtual ~Response() = default; }; using Headers = std::vector<std::pair<std::string, std::string>>; @@ -92,7 +92,7 @@ const SendRequestCallback& callback) = 0; protected: - virtual ~HttpClient() {} + virtual ~HttpClient() = default; }; } // namespace provider
diff --git a/include/weave/provider/http_server.h b/include/weave/provider/http_server.h index 1c28d63..622785b 100644 --- a/include/weave/provider/http_server.h +++ b/include/weave/provider/http_server.h
@@ -109,7 +109,7 @@ public: class Request { public: - virtual ~Request() {} + virtual ~Request() = default; virtual std::string GetPath() const = 0; virtual std::string GetFirstHeader(const std::string& name) const = 0; @@ -141,7 +141,7 @@ virtual base::TimeDelta GetRequestTimeout() const = 0; protected: - virtual ~HttpServer() {} + virtual ~HttpServer() = default; }; } // namespace provider
diff --git a/include/weave/provider/network.h b/include/weave/provider/network.h index 0fb147d..651155a 100644 --- a/include/weave/provider/network.h +++ b/include/weave/provider/network.h
@@ -47,7 +47,7 @@ const OpenSslSocketCallback& callback) = 0; protected: - virtual ~Network() {} + virtual ~Network() = default; }; } // namespace provider
diff --git a/include/weave/provider/task_runner.h b/include/weave/provider/task_runner.h index 095910b..0804a10 100644 --- a/include/weave/provider/task_runner.h +++ b/include/weave/provider/task_runner.h
@@ -28,7 +28,7 @@ base::TimeDelta delay) = 0; protected: - virtual ~TaskRunner() {} + virtual ~TaskRunner() = default; }; } // namespace provider
diff --git a/include/weave/provider/wifi.h b/include/weave/provider/wifi.h index 48ac651..111bf3c 100644 --- a/include/weave/provider/wifi.h +++ b/include/weave/provider/wifi.h
@@ -29,7 +29,7 @@ virtual void StopAccessPoint() = 0; protected: - virtual ~Wifi() {} + virtual ~Wifi() = default; }; } // namespace provider
diff --git a/include/weave/stream.h b/include/weave/stream.h index 14cc7f0..19d38a0 100644 --- a/include/weave/stream.h +++ b/include/weave/stream.h
@@ -15,7 +15,7 @@ // Interface for async input streaming. class InputStream { public: - virtual ~InputStream() {} + virtual ~InputStream() = default; // Callback type for Read. using ReadCallback = base::Callback<void(size_t size, ErrorPtr error)>; @@ -31,7 +31,7 @@ // Interface for async input streaming. class OutputStream { public: - virtual ~OutputStream() {} + virtual ~OutputStream() = default; using WriteCallback = base::Callback<void(ErrorPtr error)>; @@ -47,7 +47,7 @@ // Interface for async bi-directional streaming. class Stream : public InputStream, public OutputStream { public: - ~Stream() override {} + ~Stream() override = default; // Cancels all pending read or write requests. Canceled operations must not // call any callbacks.
diff --git a/include/weave/test/mock_command.h b/include/weave/test/mock_command.h index fe1a02a..2b1080e 100644 --- a/include/weave/test/mock_command.h +++ b/include/weave/test/mock_command.h
@@ -25,9 +25,9 @@ MOCK_CONST_METHOD0(GetCategory, const std::string&()); MOCK_CONST_METHOD0(GetState, Command::State()); MOCK_CONST_METHOD0(GetOrigin, Command::Origin()); - MOCK_CONST_METHOD0(GetParameters, const base::DictionaryValue&()); - MOCK_CONST_METHOD0(GetProgress, const base::DictionaryValue&()); - MOCK_CONST_METHOD0(GetResults, const base::DictionaryValue&()); + MOCK_CONST_METHOD0(MockGetParameters, const std::string&()); + MOCK_CONST_METHOD0(MockGetProgress, const std::string&()); + MOCK_CONST_METHOD0(MockGetResults, const std::string&()); MOCK_CONST_METHOD0(GetError, const Error*()); MOCK_METHOD2(SetProgress, bool(const base::DictionaryValue&, ErrorPtr*)); MOCK_METHOD2(Complete, bool(const base::DictionaryValue&, ErrorPtr*)); @@ -35,6 +35,10 @@ MOCK_METHOD2(SetError, bool(const Error*, ErrorPtr*)); MOCK_METHOD2(Abort, bool(const Error*, ErrorPtr*)); MOCK_METHOD1(Cancel, bool(ErrorPtr*)); + + std::unique_ptr<base::DictionaryValue> GetParameters() const override; + std::unique_ptr<base::DictionaryValue> GetProgress() const override; + std::unique_ptr<base::DictionaryValue> GetResults() const override; }; } // namespace test
diff --git a/include/weave/test/mock_device.h b/include/weave/test/mock_device.h index e5063e7..f751f97 100644 --- a/include/weave/test/mock_device.h +++ b/include/weave/test/mock_device.h
@@ -34,13 +34,13 @@ MOCK_METHOD2(SetStatePropertiesFromJson, bool(const std::string&, ErrorPtr*)); MOCK_METHOD2(SetStateProperties, bool(const base::DictionaryValue&, ErrorPtr*)); - MOCK_CONST_METHOD1(GetStateProperty, - const base::Value*(const std::string& name)); + MOCK_CONST_METHOD1(MockGetStateProperty, + base::Value*(const std::string& name)); MOCK_METHOD3(SetStateProperty, bool(const std::string& name, const base::Value& value, ErrorPtr* error)); - MOCK_CONST_METHOD0(GetState, const base::DictionaryValue&()); + MOCK_CONST_METHOD0(MockGetState, base::DictionaryValue*()); MOCK_CONST_METHOD0(GetGcdState, GcdState()); MOCK_METHOD1(AddGcdStateChangedCallback, void(const GcdStateChangedCallback& callback)); @@ -50,6 +50,15 @@ MOCK_METHOD2(AddPairingChangedCallbacks, void(const PairingBeginCallback& begin_callback, const PairingEndCallback& end_callback)); + + // Gmock 1.7.0 does not work with unuque_ptr as return value. + std::unique_ptr<base::Value> GetStateProperty( + const std::string& name) const override { + return std::unique_ptr<base::Value>(MockGetStateProperty(name)); + } + std::unique_ptr<base::DictionaryValue> GetState() const override { + return std::unique_ptr<base::DictionaryValue>(MockGetState()); + } }; } // namespace test
diff --git a/libweave.gypi b/libweave.gypi index a02f082..c32e99b 100644 --- a/libweave.gypi +++ b/libweave.gypi
@@ -12,7 +12,12 @@ 'src/commands/command_instance.cc', 'src/commands/command_manager.cc', 'src/commands/command_queue.cc', + 'src/commands/object_schema.cc', + 'src/commands/prop_constraints.cc', + 'src/commands/prop_types.cc', + 'src/commands/prop_values.cc', 'src/commands/schema_constants.cc', + 'src/commands/schema_utils.cc', 'src/config.cc', 'src/data_encoding.cc', 'src/device_manager.cc', @@ -54,6 +59,7 @@ 'weave_test_sources': [ 'src/test/fake_stream.cc', 'src/test/fake_task_runner.cc', + 'src/test/mock_command.cc', 'src/test/unittest_utils.cc', ], 'weave_unittest_sources': [ @@ -65,6 +71,8 @@ 'src/commands/command_instance_unittest.cc', 'src/commands/command_manager_unittest.cc', 'src/commands/command_queue_unittest.cc', + 'src/commands/object_schema_unittest.cc', + 'src/commands/schema_utils_unittest.cc', 'src/config_unittest.cc', 'src/data_encoding_unittest.cc', 'src/device_registration_info_unittest.cc',
diff --git a/libweave_common.gypi b/libweave_common.gypi index 2e1fa10..49f7ff9 100644 --- a/libweave_common.gypi +++ b/libweave_common.gypi
@@ -17,7 +17,7 @@ '_DEBUG', ], 'cflags': [ - '-O0 ', + '-Og', '-g3', ], }, @@ -31,7 +31,6 @@ ], 'cflags!': ['-fPIE'], 'cflags': [ - '-fno-exceptions', '-fPIC', '-fvisibility=hidden', '-std=c++11', @@ -48,10 +47,6 @@ '-Wpointer-arith', '-Wwrite-strings', ], - 'libraries': [ - # 'library_dirs' does not work as expected with make files - '-Lthird_party/lib', - ], 'library_dirs': ['third_party/lib'] }, }
diff --git a/libweave_standalone.gyp b/libweave_standalone.gyp index d36d208..fd87f16 100644 --- a/libweave_standalone.gyp +++ b/libweave_standalone.gyp
@@ -8,11 +8,10 @@ 'target_defaults': { 'libraries': [ '-lcrypto', - '-lexpat', - '-lgmock', '-lgtest', + '-lgmock', + '-lexpat', '-lpthread', - '-lrt', ], }, 'targets': [
diff --git a/src/backoff_entry.h b/src/backoff_entry.h index 002fb8d..2df0d8a 100644 --- a/src/backoff_entry.h +++ b/src/backoff_entry.h
@@ -57,7 +57,7 @@ // Lifetime of policy must enclose lifetime of BackoffEntry. The // pointer must be valid but is not dereferenced during construction. explicit BackoffEntry(const Policy* const policy); - virtual ~BackoffEntry() {} + virtual ~BackoffEntry() = default; // Inform this item that a request for the network resource it is // tracking was made, and whether it failed or succeeded.
diff --git a/src/base_api_handler.cc b/src/base_api_handler.cc index 1423dd1..c3aa616 100644 --- a/src/base_api_handler.cc +++ b/src/base_api_handler.cc
@@ -42,31 +42,20 @@ "updateBaseConfiguration": { "minimalRole": "manager", "parameters": { - "localAnonymousAccessMaxRole": { - "enum": [ "none", "viewer", "user" ], - "type": "string" - }, - "localDiscoveryEnabled": { - "type": "boolean" - }, - "localPairingEnabled": { - "type": "boolean" - } - } + "localDiscoveryEnabled": "boolean", + "localAnonymousAccessMaxRole": [ "none", "viewer", "user" ], + "localPairingEnabled": "boolean" + }, + "results": {} }, "updateDeviceInfo": { "minimalRole": "manager", "parameters": { - "description": { - "type": "string" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - } - } + "description": "string", + "name": "string", + "location": "string" + }, + "results": {} } } })"); @@ -99,10 +88,10 @@ bool discovery_enabled{settings.local_discovery_enabled}; bool pairing_enabled{settings.local_pairing_enabled}; - const auto& parameters = command->GetParameters(); - parameters.GetString("localAnonymousAccessMaxRole", &anonymous_access_role); - parameters.GetBoolean("localDiscoveryEnabled", &discovery_enabled); - parameters.GetBoolean("localPairingEnabled", &pairing_enabled); + auto parameters = command->GetParameters(); + parameters->GetString("localAnonymousAccessMaxRole", &anonymous_access_role); + parameters->GetBoolean("localDiscoveryEnabled", &discovery_enabled); + parameters->GetBoolean("localPairingEnabled", &pairing_enabled); AuthScope auth_scope{AuthScope::kNone}; if (!StringToEnum(anonymous_access_role, &auth_scope)) { @@ -144,10 +133,10 @@ std::string description{settings.description}; std::string location{settings.location}; - const auto& parameters = command->GetParameters(); - parameters.GetString("name", &name); - parameters.GetString("description", &description); - parameters.GetString("location", &location); + auto parameters = command->GetParameters(); + parameters->GetString("name", &name); + parameters->GetString("description", &description); + parameters->GetString("location", &location); device_info_->UpdateDeviceInfo(name, description, location); command->Complete({}, nullptr);
diff --git a/src/base_api_handler_unittest.cc b/src/base_api_handler_unittest.cc index 15a575a..a025e44 100644 --- a/src/base_api_handler_unittest.cc +++ b/src/base_api_handler_unittest.cc
@@ -10,9 +10,9 @@ #include <weave/provider/test/mock_config_store.h> #include <weave/provider/test/mock_http_client.h> #include <weave/test/mock_device.h> -#include <weave/test/unittest_utils.h> #include "src/commands/command_manager.h" +#include "src/commands/unittest_utils.h" #include "src/config.h" #include "src/device_registration_info.h" #include "src/states/mock_state_change_queue_interface.h" @@ -31,7 +31,7 @@ class BaseApiHandlerTest : public ::testing::Test { protected: void SetUp() override { - EXPECT_CALL(mock_state_change_queue_, MockNotifyPropertiesUpdated(_, _)) + EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(_, _)) .WillRepeatedly(Return(true)); command_manager_ = std::make_shared<CommandManager>(); @@ -84,8 +84,7 @@ } std::unique_ptr<base::DictionaryValue> GetBaseState() { - std::unique_ptr<base::DictionaryValue> state{ - state_manager_->GetState().DeepCopy()}; + auto state = state_manager_->GetState(); std::set<std::string> result; for (base::DictionaryValue::Iterator it{*state}; !it.IsAtEnd(); it.Advance()) { @@ -108,38 +107,39 @@ TEST_F(BaseApiHandlerTest, Initialization) { auto command_defs = - command_manager_->GetCommandDictionary().GetCommandsAsJson(nullptr); + command_manager_->GetCommandDictionary().GetCommandsAsJson( + [](const CommandDefinition* def) { return true; }, true, nullptr); auto expected = R"({ "base": { "updateBaseConfiguration": { - "minimalRole": "manager", - "parameters": { - "localAnonymousAccessMaxRole": { - "enum": [ "none", "viewer", "user" ], - "type": "string" - }, - "localDiscoveryEnabled": { - "type": "boolean" - }, - "localPairingEnabled": { - "type": "boolean" - } - } + "minimalRole": "manager", + "parameters": { + "localAnonymousAccessMaxRole": { + "enum": [ "none", "viewer", "user" ], + "type": "string" + }, + "localDiscoveryEnabled": { + "type": "boolean" + }, + "localPairingEnabled": { + "type": "boolean" + } + } }, "updateDeviceInfo": { - "minimalRole": "manager", - "parameters": { - "description": { - "type": "string" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - } - } + "minimalRole": "manager", + "parameters": { + "description": { + "type": "string" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + } + } } } })";
diff --git a/src/commands/cloud_command_proxy.cc b/src/commands/cloud_command_proxy.cc index 3d472c7..9ec3d3e 100644 --- a/src/commands/cloud_command_proxy.cc +++ b/src/commands/cloud_command_proxy.cc
@@ -9,6 +9,8 @@ #include <weave/provider/task_runner.h> #include "src/commands/command_instance.h" +#include "src/commands/prop_constraints.h" +#include "src/commands/prop_types.h" #include "src/commands/schema_constants.h" #include "src/utils.h" @@ -43,7 +45,7 @@ void CloudCommandProxy::OnResultsChanged() { std::unique_ptr<base::DictionaryValue> patch{new base::DictionaryValue}; patch->Set(commands::attributes::kCommand_Results, - command_instance_->GetResults().CreateDeepCopy()); + command_instance_->GetResults().release()); QueueCommandUpdate(std::move(patch)); } @@ -57,7 +59,7 @@ void CloudCommandProxy::OnProgressChanged() { std::unique_ptr<base::DictionaryValue> patch{new base::DictionaryValue}; patch->Set(commands::attributes::kCommand_Progress, - command_instance_->GetProgress().CreateDeepCopy()); + command_instance_->GetProgress().release()); QueueCommandUpdate(std::move(patch)); }
diff --git a/src/commands/cloud_command_proxy_unittest.cc b/src/commands/cloud_command_proxy_unittest.cc index a65a967..0c04592 100644 --- a/src/commands/cloud_command_proxy_unittest.cc +++ b/src/commands/cloud_command_proxy_unittest.cc
@@ -10,10 +10,10 @@ #include <gmock/gmock.h> #include <gtest/gtest.h> #include <weave/provider/test/fake_task_runner.h> -#include <weave/test/unittest_utils.h> #include "src/commands/command_dictionary.h" #include "src/commands/command_instance.h" +#include "src/commands/unittest_utils.h" #include "src/states/mock_state_change_queue_interface.h" using testing::_; @@ -94,7 +94,7 @@ } })"); CHECK(json.get()); - CHECK(command_dictionary_.LoadCommands(*json, nullptr)) + CHECK(command_dictionary_.LoadCommands(*json, nullptr, nullptr)) << "Failed to parse test command dictionary"; CreateCommandInstance();
diff --git a/src/commands/cloud_command_update_interface.h b/src/commands/cloud_command_update_interface.h index ed3aa7a..9538960 100644 --- a/src/commands/cloud_command_update_interface.h +++ b/src/commands/cloud_command_update_interface.h
@@ -21,7 +21,7 @@ const DoneCallback& callback) = 0; protected: - virtual ~CloudCommandUpdateInterface() {} + virtual ~CloudCommandUpdateInterface() = default; }; } // namespace weave
diff --git a/src/commands/command_definition.cc b/src/commands/command_definition.cc index dbae630..d7ebc83 100644 --- a/src/commands/command_definition.cc +++ b/src/commands/command_definition.cc
@@ -28,31 +28,61 @@ LIBWEAVE_EXPORT EnumToStringMap<UserRole>::EnumToStringMap() : EnumToStringMap(kMap) {} -CommandDefinition::CommandDefinition(const base::DictionaryValue& definition, - UserRole minimal_role) - : minimal_role_{minimal_role} { - definition_.MergeDictionary(&definition); +bool CommandDefinition::Visibility::FromString(const std::string& str, + ErrorPtr* error) { + // This special case is useful for places where we want to make a command + // to ALL clients, even if new clients are added in the future. + if (str == commands::attributes::kCommand_Visibility_All) { + local = true; + cloud = true; + return true; + } + + // Clear any bits first. + local = false; + cloud = false; + if (str == commands::attributes::kCommand_Visibility_None) + return true; + + for (const std::string& value : Split(str, ",", true, true)) { + if (value == commands::attributes::kCommand_Visibility_Local) { + local = true; + } else if (value == commands::attributes::kCommand_Visibility_Cloud) { + cloud = true; + } else { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidPropValue, + "Invalid command visibility value '%s'", + value.c_str()); + return false; + } + } + return true; } -std::unique_ptr<CommandDefinition> CommandDefinition::FromJson( - const base::DictionaryValue& dict, ErrorPtr* error) { - std::unique_ptr<CommandDefinition> definition; - // Validate the 'minimalRole' value if present. That's the only thing we - // care about so far. - std::string value; - UserRole minimal_role; - if (dict.GetString(commands::attributes::kCommand_Role, &value)) { - if (!StringToEnum(value, &minimal_role)) { - Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, - errors::commands::kInvalidPropValue, - "Invalid role: '%s'", value.c_str()); - return definition; - } - } else { - minimal_role = UserRole::kUser; - } - definition.reset(new CommandDefinition{dict, minimal_role}); - return definition; +std::string CommandDefinition::Visibility::ToString() const { + if (local && cloud) + return commands::attributes::kCommand_Visibility_All; + if (!local && !cloud) + return commands::attributes::kCommand_Visibility_None; + if (local) + return commands::attributes::kCommand_Visibility_Local; + return commands::attributes::kCommand_Visibility_Cloud; +} + +CommandDefinition::CommandDefinition( + std::unique_ptr<const ObjectSchema> parameters, + std::unique_ptr<const ObjectSchema> progress, + std::unique_ptr<const ObjectSchema> results) + : parameters_{std::move(parameters)}, + progress_{std::move(progress)}, + results_{std::move(results)} { + // Set to be available to all clients by default. + visibility_ = Visibility::GetAll(); +} + +void CommandDefinition::SetVisibility(const Visibility& visibility) { + visibility_ = visibility; } } // namespace weave
diff --git a/src/commands/command_definition.h b/src/commands/command_definition.h index da02bf5..3bcc07f 100644 --- a/src/commands/command_definition.h +++ b/src/commands/command_definition.h
@@ -9,8 +9,8 @@ #include <string> #include <base/macros.h> -#include <base/values.h> -#include <weave/error.h> + +#include "src/commands/object_schema.h" namespace weave { @@ -25,19 +25,55 @@ // describing the command parameter types and constraints. class CommandDefinition final { public: - // Factory method to construct a command definition from a JSON dictionary. - static std::unique_ptr<CommandDefinition> FromJson( - const base::DictionaryValue& dict, ErrorPtr* error); - const base::DictionaryValue& ToJson() const { return definition_; } + struct Visibility { + Visibility() = default; + Visibility(bool is_local, bool is_cloud) + : local{is_local}, cloud{is_cloud} {} + + // Converts a comma-separated string of visibility identifiers into the + // Visibility bitset (|str| is a string like "local,cloud"). + // Special string value "all" is treated as a list of every possible + // visibility values and "none" to have all the bits cleared. + bool FromString(const std::string& str, ErrorPtr* error); + + // Converts the visibility bitset to a string. + std::string ToString() const; + + static Visibility GetAll() { return Visibility{true, true}; } + static Visibility GetLocal() { return Visibility{true, false}; } + static Visibility GetCloud() { return Visibility{false, true}; } + static Visibility GetNone() { return Visibility{false, false}; } + + bool local{false}; // Command is available to local clients. + bool cloud{false}; // Command is available to cloud clients. + }; + + CommandDefinition(std::unique_ptr<const ObjectSchema> parameters, + std::unique_ptr<const ObjectSchema> progress, + std::unique_ptr<const ObjectSchema> results); + + // Gets the object schema for command parameters. + const ObjectSchema* GetParameters() const { return parameters_.get(); } + // Gets the object schema for command progress. + const ObjectSchema* GetProgress() const { return progress_.get(); } + // Gets the object schema for command results. + const ObjectSchema* GetResults() const { return results_.get(); } + // Returns the command visibility. + const Visibility& GetVisibility() const { return visibility_; } + // Changes the command visibility. + void SetVisibility(const Visibility& visibility); // Returns the role required to execute command. UserRole GetMinimalRole() const { return minimal_role_; } + // Changes the role required to execute command. + void SetMinimalRole(UserRole minimal_role) { minimal_role_ = minimal_role; } private: - CommandDefinition(const base::DictionaryValue& definition, - UserRole minimal_role); - - base::DictionaryValue definition_; - UserRole minimal_role_; + std::unique_ptr<const ObjectSchema> parameters_; // Command parameters def. + std::unique_ptr<const ObjectSchema> progress_; // Command progress def. + std::unique_ptr<const ObjectSchema> results_; // Command results def. + Visibility visibility_; // Available to all by default. + // Minimal role required to execute command. + UserRole minimal_role_{UserRole::kUser}; DISALLOW_COPY_AND_ASSIGN(CommandDefinition); };
diff --git a/src/commands/command_definition_unittest.cc b/src/commands/command_definition_unittest.cc index 867d48f..77b2754 100644 --- a/src/commands/command_definition_unittest.cc +++ b/src/commands/command_definition_unittest.cc
@@ -5,47 +5,88 @@ #include "src/commands/command_definition.h" #include <gtest/gtest.h> -#include <weave/test/unittest_utils.h> namespace weave { -using test::CreateDictionaryValue; - -TEST(CommandDefinition, DefaultRole) { - auto params = CreateDictionaryValue(R"({ - 'parameters': { - 'height': 'integer', - 'jumpType': ['_withAirFlip', '_withSpin', '_withKick'] - }, - 'progress': {'progress': 'integer'}, - 'results': {'testResult': 'integer'} - })"); - auto def = CommandDefinition::FromJson(*params, nullptr); - EXPECT_EQ(UserRole::kUser, def->GetMinimalRole()); +TEST(CommandVisibility, DefaultConstructor) { + CommandDefinition::Visibility visibility; + EXPECT_FALSE(visibility.local); + EXPECT_FALSE(visibility.cloud); } -TEST(CommandDefinition, SpecifiedRole) { - auto params = CreateDictionaryValue(R"({ - 'parameters': {}, - 'progress': {}, - 'results': {}, - 'minimalRole': 'owner' - })"); - auto def = CommandDefinition::FromJson(*params, nullptr); - EXPECT_EQ(UserRole::kOwner, def->GetMinimalRole()); +TEST(CommandVisibility, InitialState) { + auto visibility = CommandDefinition::Visibility::GetAll(); + EXPECT_TRUE(visibility.local); + EXPECT_TRUE(visibility.cloud); + + visibility = CommandDefinition::Visibility::GetLocal(); + EXPECT_TRUE(visibility.local); + EXPECT_FALSE(visibility.cloud); + + visibility = CommandDefinition::Visibility::GetCloud(); + EXPECT_FALSE(visibility.local); + EXPECT_TRUE(visibility.cloud); + + visibility = CommandDefinition::Visibility::GetNone(); + EXPECT_FALSE(visibility.local); + EXPECT_FALSE(visibility.cloud); } -TEST(CommandDefinition, IncorrectRole) { - auto params = CreateDictionaryValue(R"({ - 'parameters': {}, - 'progress': {}, - 'results': {}, - 'minimalRole': 'foo' - })"); +TEST(CommandVisibility, FromString) { + CommandDefinition::Visibility visibility; + + ASSERT_TRUE(visibility.FromString("local", nullptr)); + EXPECT_TRUE(visibility.local); + EXPECT_FALSE(visibility.cloud); + + ASSERT_TRUE(visibility.FromString("cloud", nullptr)); + EXPECT_FALSE(visibility.local); + EXPECT_TRUE(visibility.cloud); + + ASSERT_TRUE(visibility.FromString("cloud,local", nullptr)); + EXPECT_TRUE(visibility.local); + EXPECT_TRUE(visibility.cloud); + + ASSERT_TRUE(visibility.FromString("none", nullptr)); + EXPECT_FALSE(visibility.local); + EXPECT_FALSE(visibility.cloud); + + ASSERT_TRUE(visibility.FromString("all", nullptr)); + EXPECT_TRUE(visibility.local); + EXPECT_TRUE(visibility.cloud); + + ASSERT_TRUE(visibility.FromString("", nullptr)); + EXPECT_FALSE(visibility.local); + EXPECT_FALSE(visibility.cloud); + ErrorPtr error; - auto def = CommandDefinition::FromJson(*params, &error); - EXPECT_EQ(nullptr, def.get()); + ASSERT_FALSE(visibility.FromString("cloud,all", &error)); EXPECT_EQ("invalid_parameter_value", error->GetCode()); } +TEST(CommandVisibility, ToString) { + EXPECT_EQ("none", CommandDefinition::Visibility::GetNone().ToString()); + EXPECT_EQ("local", CommandDefinition::Visibility::GetLocal().ToString()); + EXPECT_EQ("cloud", CommandDefinition::Visibility::GetCloud().ToString()); + EXPECT_EQ("all", CommandDefinition::Visibility::GetAll().ToString()); +} + +TEST(CommandDefinition, Test) { + std::unique_ptr<const ObjectSchema> params{ObjectSchema::Create()}; + std::unique_ptr<const ObjectSchema> progress{ObjectSchema::Create()}; + std::unique_ptr<const ObjectSchema> results{ObjectSchema::Create()}; + const ObjectSchema* param_ptr = params.get(); + const ObjectSchema* progress_ptr = progress.get(); + const ObjectSchema* results_ptr = results.get(); + CommandDefinition def{std::move(params), std::move(progress), + std::move(results)}; + EXPECT_EQ(param_ptr, def.GetParameters()); + EXPECT_EQ(progress_ptr, def.GetProgress()); + EXPECT_EQ(results_ptr, def.GetResults()); + EXPECT_EQ("all", def.GetVisibility().ToString()); + + def.SetVisibility(CommandDefinition::Visibility::GetLocal()); + EXPECT_EQ("local", def.GetVisibility().ToString()); +} + } // namespace weave
diff --git a/src/commands/command_dictionary.cc b/src/commands/command_dictionary.cc index 516961f..053e7aa 100644 --- a/src/commands/command_dictionary.cc +++ b/src/commands/command_dictionary.cc
@@ -14,6 +14,7 @@ namespace weave { bool CommandDictionary::LoadCommands(const base::DictionaryValue& json, + const CommandDictionary* base_commands, ErrorPtr* error) { CommandMap new_defs; @@ -53,17 +54,89 @@ // Construct the compound command name as "pkg_name.cmd_name". std::string full_command_name = Join(".", package_name, command_name); - auto command_def = CommandDefinition::FromJson(*command_def_json, error); - if (!command_def) { - Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, - errors::commands::kInvalidMinimalRole, - "Error parsing command '%s'", - full_command_name.c_str()); - return false; + const ObjectSchema* base_parameters_def = nullptr; + const ObjectSchema* base_progress_def = nullptr; + const ObjectSchema* base_results_def = nullptr; + // By default make it available to all clients. + auto visibility = CommandDefinition::Visibility::GetAll(); + UserRole minimal_role{UserRole::kUser}; + if (base_commands) { + auto cmd = base_commands->FindCommand(full_command_name); + if (cmd) { + base_parameters_def = cmd->GetParameters(); + base_progress_def = cmd->GetProgress(); + base_results_def = cmd->GetResults(); + visibility = cmd->GetVisibility(); + minimal_role = cmd->GetMinimalRole(); + } + + // If the base command dictionary was provided but the command was not + // found in it, this must be a custom (vendor) command. GCD spec states + // that all custom command names must begin with "_". Let's enforce + // this rule here. + if (!cmd) { + if (command_name.front() != '_') { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidCommandName, + "The name of custom command '%s' in package '%s'" + " must start with '_'", + command_name.c_str(), package_name.c_str()); + return false; + } + } } - new_defs.insert( - std::make_pair(full_command_name, std::move(command_def))); + auto parameters_schema = BuildObjectSchema( + command_def_json, commands::attributes::kCommand_Parameters, + base_parameters_def, full_command_name, error); + if (!parameters_schema) + return false; + + auto progress_schema = BuildObjectSchema( + command_def_json, commands::attributes::kCommand_Progress, + base_progress_def, full_command_name, error); + if (!progress_schema) + return false; + + auto results_schema = BuildObjectSchema( + command_def_json, commands::attributes::kCommand_Results, + base_results_def, full_command_name, error); + if (!results_schema) + return false; + + std::string value; + if (command_def_json->GetString(commands::attributes::kCommand_Visibility, + &value)) { + if (!visibility.FromString(value, error)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidCommandVisibility, + "Error parsing command '%s'", + full_command_name.c_str()); + return false; + } + } + + if (command_def_json->GetString(commands::attributes::kCommand_Role, + &value)) { + if (!StringToEnum(value, &minimal_role)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidPropValue, + "Invalid role: '%s'", value.c_str()); + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidMinimalRole, + "Error parsing command '%s'", + full_command_name.c_str()); + return false; + } + } + + std::unique_ptr<CommandDefinition> command_def{new CommandDefinition{ + std::move(parameters_schema), std::move(progress_schema), + std::move(results_schema)}}; + command_def->SetVisibility(visibility); + command_def->SetMinimalRole(minimal_role); + new_defs.emplace(full_command_name, std::move(command_def)); + command_iter.Advance(); } package_iter.Advance(); @@ -82,14 +155,53 @@ // Insert new definitions into the global map. for (auto& pair : new_defs) - definitions_.insert(std::make_pair(pair.first, std::move(pair.second))); + definitions_.emplace(pair.first, std::move(pair.second)); return true; } +std::unique_ptr<ObjectSchema> CommandDictionary::BuildObjectSchema( + const base::DictionaryValue* command_def_json, + const char* property_name, + const ObjectSchema* base_def, + const std::string& command_name, + ErrorPtr* error) { + auto object_schema = ObjectSchema::Create(); + + const base::DictionaryValue* schema_def = nullptr; + if (!command_def_json->GetDictionaryWithoutPathExpansion(property_name, + &schema_def)) { + if (base_def) + return base_def->Clone(); + return object_schema; + } + + if (!object_schema->FromJson(schema_def, base_def, error)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidObjectSchema, + "Invalid definition for command '%s'", + command_name.c_str()); + return {}; + } + + return object_schema; +} + std::unique_ptr<base::DictionaryValue> CommandDictionary::GetCommandsAsJson( + const std::function<bool(const CommandDefinition*)>& filter, + bool full_schema, ErrorPtr* error) const { std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue); for (const auto& pair : definitions_) { + // Check if the command definition has the desired visibility. + // If not, then skip it. + if (!filter(pair.second.get())) + continue; + + std::unique_ptr<base::DictionaryValue> parameters = + pair.second->GetParameters()->ToJson(full_schema, true); + CHECK(parameters); + // Progress and results are not part of public commandDefs. + auto parts = SplitAtFirst(pair.first, ".", true); const std::string& package_name = parts.first; const std::string& command_name = parts.second; @@ -101,8 +213,12 @@ package = new base::DictionaryValue; dict->SetWithoutPathExpansion(package_name, package); } - package->SetWithoutPathExpansion(command_name, - pair.second->ToJson().DeepCopy()); + base::DictionaryValue* command_def = new base::DictionaryValue; + command_def->Set(commands::attributes::kCommand_Parameters, + parameters.release()); + command_def->SetString(commands::attributes::kCommand_Role, + EnumToString(pair.second->GetMinimalRole())); + package->SetWithoutPathExpansion(command_name, command_def); } return dict; }
diff --git a/src/commands/command_dictionary.h b/src/commands/command_dictionary.h index 03e080a..8d3d45c 100644 --- a/src/commands/command_dictionary.h +++ b/src/commands/command_dictionary.h
@@ -23,6 +23,8 @@ namespace weave { +class ObjectSchema; + // CommandDictionary is a wrapper around a map of command name and the // corresponding command definition schema. The command name (the key in // the map) is a compound name in a form of "package_name.command_name", @@ -35,15 +37,25 @@ // Loads command definitions from a JSON object. This is done at the daemon // startup and whenever a device daemon decides to update its command list. // |json| is a JSON dictionary that describes the complete commands. Optional - // Returns false on failure and |error| provides additional error information - // when provided. + // |base_commands| parameter specifies the definition of standard GCD commands + // for parameter schema validation. Can be set to nullptr if no validation is + // needed. Returns false on failure and |error| provides additional error + // information when provided. bool LoadCommands(const base::DictionaryValue& json, + const CommandDictionary* base_commands, ErrorPtr* error); // Converts all the command definitions to a JSON object for CDD/Device // draft. + // |filter| is a predicate used to filter out the command definitions to + // be returned by this method. Only command definitions for which the + // predicate returns true will be included in the resulting JSON. + // |full_schema| specifies whether full command definitions must be generated + // (true) for CDD or only overrides from the base schema (false). // Returns empty unique_ptr in case of an error and fills in the additional // error details in |error|. std::unique_ptr<base::DictionaryValue> GetCommandsAsJson( + const std::function<bool(const CommandDefinition*)>& filter, + bool full_schema, ErrorPtr* error) const; // Returns the number of command definitions in the dictionary. size_t GetSize() const { return definitions_.size(); } @@ -58,6 +70,13 @@ private: using CommandMap = std::map<std::string, std::unique_ptr<CommandDefinition>>; + std::unique_ptr<ObjectSchema> BuildObjectSchema( + const base::DictionaryValue* command_def_json, + const char* property_name, + const ObjectSchema* base_def, + const std::string& command_name, + ErrorPtr* error); + CommandMap definitions_; // List of all available command definitions. DISALLOW_COPY_AND_ASSIGN(CommandDictionary); };
diff --git a/src/commands/command_dictionary_unittest.cc b/src/commands/command_dictionary_unittest.cc index adae4ec..819718f 100644 --- a/src/commands/command_dictionary_unittest.cc +++ b/src/commands/command_dictionary_unittest.cc
@@ -5,7 +5,8 @@ #include "src/commands/command_dictionary.h" #include <gtest/gtest.h> -#include <weave/test/unittest_utils.h> + +#include "src/commands/unittest_utils.h" namespace weave { @@ -34,8 +35,8 @@ } })"); CommandDictionary dict; - EXPECT_TRUE(dict.LoadCommands(*json, nullptr)); - EXPECT_EQ(1u, dict.GetSize()); + EXPECT_TRUE(dict.LoadCommands(*json, nullptr, nullptr)); + EXPECT_EQ(1, dict.GetSize()); EXPECT_NE(nullptr, dict.FindCommand("robot.jump")); json = CreateDictionaryValue(R"({ 'base': { @@ -46,49 +47,185 @@ } } })"); - EXPECT_TRUE(dict.LoadCommands(*json, nullptr)); - EXPECT_EQ(3u, dict.GetSize()); + EXPECT_TRUE(dict.LoadCommands(*json, nullptr, nullptr)); + EXPECT_EQ(3, dict.GetSize()); EXPECT_NE(nullptr, dict.FindCommand("robot.jump")); EXPECT_NE(nullptr, dict.FindCommand("base.reboot")); EXPECT_NE(nullptr, dict.FindCommand("base.shutdown")); EXPECT_EQ(nullptr, dict.FindCommand("foo.bar")); } +TEST(CommandDictionary, LoadWithInheritance) { + auto json = CreateDictionaryValue(R"({ + 'robot': { + 'jump': { + 'minimalRole': 'viewer', + 'visibility':'local', + 'parameters': { + 'height': 'integer' + }, + 'progress': { + 'progress': 'integer' + }, + 'results': { + 'success': 'boolean' + } + } + } + })"); + CommandDictionary base_dict; + EXPECT_TRUE(base_dict.LoadCommands(*json, nullptr, nullptr)); + EXPECT_EQ(1, base_dict.GetSize()); + json = CreateDictionaryValue(R"({'robot': {'jump': {}}})"); + + CommandDictionary dict; + EXPECT_TRUE(dict.LoadCommands(*json, &base_dict, nullptr)); + EXPECT_EQ(1, dict.GetSize()); + + auto cmd = dict.FindCommand("robot.jump"); + EXPECT_NE(nullptr, cmd); + + EXPECT_EQ("local", cmd->GetVisibility().ToString()); + EXPECT_EQ(UserRole::kViewer, cmd->GetMinimalRole()); + + EXPECT_JSON_EQ("{'height': {'type': 'integer'}}", + *cmd->GetParameters()->ToJson(true, true)); + EXPECT_JSON_EQ("{'progress': {'type': 'integer'}}", + *cmd->GetProgress()->ToJson(true, false)); + EXPECT_JSON_EQ("{'success': {'type': 'boolean'}}", + *cmd->GetResults()->ToJson(true, false)); +} + TEST(CommandDictionary, LoadCommands_Failures) { CommandDictionary dict; ErrorPtr error; // Command definition is not an object. auto json = CreateDictionaryValue("{'robot':{'jump':0}}"); - EXPECT_FALSE(dict.LoadCommands(*json, &error)); + EXPECT_FALSE(dict.LoadCommands(*json, nullptr, &error)); EXPECT_EQ("type_mismatch", error->GetCode()); error.reset(); // Package definition is not an object. json = CreateDictionaryValue("{'robot':'blah'}"); - EXPECT_FALSE(dict.LoadCommands(*json, &error)); + EXPECT_FALSE(dict.LoadCommands(*json, nullptr, &error)); EXPECT_EQ("type_mismatch", error->GetCode()); error.reset(); + // Invalid command definition is not an object. + json = CreateDictionaryValue( + "{'robot':{'jump':{'parameters':{'flip':0},'results':{}}}}"); + EXPECT_FALSE(dict.LoadCommands(*json, nullptr, &error)); + EXPECT_EQ("invalid_object_schema", error->GetCode()); + EXPECT_NE(nullptr, error->GetInnerError()); // Must have additional info. + error.reset(); + // Empty command name. json = CreateDictionaryValue("{'robot':{'':{'parameters':{},'results':{}}}}"); - EXPECT_FALSE(dict.LoadCommands(*json, &error)); + EXPECT_FALSE(dict.LoadCommands(*json, nullptr, &error)); EXPECT_EQ("invalid_command_name", error->GetCode()); error.reset(); } -TEST(CommandDictionaryDeathTest, LoadCommands_Redefine) { - // Redefine commands. +TEST(CommandDictionaryDeathTest, LoadCommands_RedefineInDifferentCategory) { + // Redefine commands in different category. CommandDictionary dict; ErrorPtr error; auto json = CreateDictionaryValue("{'robot':{'jump':{}}}"); - dict.LoadCommands(*json, nullptr); - ASSERT_DEATH(dict.LoadCommands(*json, &error), + dict.LoadCommands(*json, nullptr, &error); + ASSERT_DEATH(dict.LoadCommands(*json, nullptr, &error), ".*Definition for command 'robot.jump' overrides an " "earlier definition"); } +TEST(CommandDictionary, LoadCommands_CustomCommandNaming) { + // Custom command must start with '_'. + CommandDictionary base_dict; + CommandDictionary dict; + ErrorPtr error; + auto json = CreateDictionaryValue(R"({ + 'base': { + 'reboot': { + 'parameters': {'delay': 'integer'}, + 'results': {} + } + } + })"); + base_dict.LoadCommands(*json, nullptr, &error); + EXPECT_TRUE(dict.LoadCommands(*json, &base_dict, &error)); + auto json2 = + CreateDictionaryValue("{'base':{'jump':{'parameters':{},'results':{}}}}"); + EXPECT_FALSE(dict.LoadCommands(*json2, &base_dict, &error)); + EXPECT_EQ("invalid_command_name", error->GetCode()); + error.reset(); + + // If the command starts with "_", then it's Ok. + json2 = CreateDictionaryValue( + "{'base':{'_jump':{'parameters':{},'results':{}}}}"); + EXPECT_TRUE(dict.LoadCommands(*json2, &base_dict, nullptr)); +} + +TEST(CommandDictionary, LoadCommands_RedefineStdCommand) { + // Redefine commands parameter type. + CommandDictionary base_dict; + CommandDictionary dict; + ErrorPtr error; + auto json = CreateDictionaryValue(R"({ + 'base': { + 'reboot': { + 'parameters': {'delay': 'integer'}, + 'results': {'version': 'integer'} + } + } + })"); + base_dict.LoadCommands(*json, nullptr, &error); + + auto json2 = CreateDictionaryValue(R"({ + 'base': { + 'reboot': { + 'parameters': {'delay': 'string'}, + 'results': {'version': 'integer'} + } + } + })"); + EXPECT_FALSE(dict.LoadCommands(*json2, &base_dict, &error)); + EXPECT_EQ("invalid_object_schema", error->GetCode()); + EXPECT_EQ("invalid_parameter_definition", error->GetInnerError()->GetCode()); + EXPECT_EQ("param_type_changed", error->GetFirstError()->GetCode()); + error.reset(); + + auto json3 = CreateDictionaryValue(R"({ + 'base': { + 'reboot': { + 'parameters': {'delay': 'integer'}, + 'results': {'version': 'string'} + } + } + })"); + EXPECT_FALSE(dict.LoadCommands(*json3, &base_dict, &error)); + EXPECT_EQ("invalid_object_schema", error->GetCode()); + // TODO(antonm): remove parameter from error below and use some generic. + EXPECT_EQ("invalid_parameter_definition", error->GetInnerError()->GetCode()); + EXPECT_EQ("param_type_changed", error->GetFirstError()->GetCode()); + error.reset(); +} + TEST(CommandDictionary, GetCommandsAsJson) { + auto json_base = CreateDictionaryValue(R"({ + 'base': { + 'reboot': { + 'parameters': {'delay': {'maximum': 100}}, + 'results': {} + }, + 'shutdown': { + 'parameters': {}, + 'results': {} + } + } + })"); + CommandDictionary base_dict; + base_dict.LoadCommands(*json_base, nullptr, nullptr); + auto json = CreateDictionaryValue(R"({ 'base': { 'reboot': { @@ -99,20 +236,21 @@ 'robot': { '_jump': { 'parameters': {'_height': 'integer'}, - 'minimalRole': 'user' + 'results': {} } } })"); CommandDictionary dict; - dict.LoadCommands(*json, nullptr); + dict.LoadCommands(*json, &base_dict, nullptr); - json = dict.GetCommandsAsJson(nullptr); + json = dict.GetCommandsAsJson( + [](const CommandDefinition* def) { return true; }, false, nullptr); ASSERT_NE(nullptr, json.get()); auto expected = R"({ 'base': { 'reboot': { 'parameters': {'delay': {'minimum': 10}}, - 'results': {} + 'minimalRole': 'user' } }, 'robot': { @@ -123,6 +261,143 @@ } })"; EXPECT_JSON_EQ(expected, *json); + + json = dict.GetCommandsAsJson( + [](const CommandDefinition* def) { return true; }, true, nullptr); + ASSERT_NE(nullptr, json.get()); + expected = R"({ + 'base': { + 'reboot': { + 'parameters': { + 'delay': { + 'maximum': 100, + 'minimum': 10, + 'type': 'integer' + } + }, + 'minimalRole': 'user' + } + }, + 'robot': { + '_jump': { + 'parameters': { + '_height': { + 'type': 'integer' + } + }, + 'minimalRole': 'user' + } + } + })"; + EXPECT_JSON_EQ(expected, *json); +} + +TEST(CommandDictionary, GetCommandsAsJsonWithVisibility) { + auto json = CreateDictionaryValue(R"({ + 'test': { + 'command1': { + 'parameters': {}, + 'results': {}, + 'visibility': 'none' + }, + 'command2': { + 'parameters': {}, + 'results': {}, + 'visibility': 'local' + }, + 'command3': { + 'parameters': {}, + 'results': {}, + 'visibility': 'cloud' + }, + 'command4': { + 'parameters': {}, + 'results': {}, + 'visibility': 'all' + }, + 'command5': { + 'parameters': {}, + 'results': {}, + 'visibility': 'none' + }, + 'command6': { + 'parameters': {}, + 'results': {}, + 'visibility': 'local' + }, + 'command7': { + 'parameters': {}, + 'results': {}, + 'visibility': 'cloud' + }, + 'command8': { + 'parameters': {}, + 'results': {}, + 'visibility': 'all' + } + } + })"); + CommandDictionary dict; + ASSERT_TRUE(dict.LoadCommands(*json, nullptr, nullptr)); + + json = dict.GetCommandsAsJson( + [](const CommandDefinition* def) { return true; }, false, nullptr); + ASSERT_NE(nullptr, json.get()); + auto expected = R"({ + 'test': { + 'command1': {'parameters': {}, 'minimalRole': 'user'}, + 'command2': {'parameters': {}, 'minimalRole': 'user'}, + 'command3': {'parameters': {}, 'minimalRole': 'user'}, + 'command4': {'parameters': {}, 'minimalRole': 'user'}, + 'command5': {'parameters': {}, 'minimalRole': 'user'}, + 'command6': {'parameters': {}, 'minimalRole': 'user'}, + 'command7': {'parameters': {}, 'minimalRole': 'user'}, + 'command8': {'parameters': {}, 'minimalRole': 'user'} + } + })"; + EXPECT_JSON_EQ(expected, *json); + + json = dict.GetCommandsAsJson( + [](const CommandDefinition* def) { return def->GetVisibility().local; }, + false, nullptr); + ASSERT_NE(nullptr, json.get()); + expected = R"({ + 'test': { + 'command2': {'parameters': {}, 'minimalRole': 'user'}, + 'command4': {'parameters': {}, 'minimalRole': 'user'}, + 'command6': {'parameters': {}, 'minimalRole': 'user'}, + 'command8': {'parameters': {}, 'minimalRole': 'user'} + } + })"; + EXPECT_JSON_EQ(expected, *json); + + json = dict.GetCommandsAsJson( + [](const CommandDefinition* def) { return def->GetVisibility().cloud; }, + false, nullptr); + ASSERT_NE(nullptr, json.get()); + expected = R"({ + 'test': { + 'command3': {'parameters': {}, 'minimalRole': 'user'}, + 'command4': {'parameters': {}, 'minimalRole': 'user'}, + 'command7': {'parameters': {}, 'minimalRole': 'user'}, + 'command8': {'parameters': {}, 'minimalRole': 'user'} + } + })"; + EXPECT_JSON_EQ(expected, *json); + + json = dict.GetCommandsAsJson( + [](const CommandDefinition* def) { + return def->GetVisibility().local && def->GetVisibility().cloud; + }, + false, nullptr); + ASSERT_NE(nullptr, json.get()); + expected = R"({ + 'test': { + 'command4': {'parameters': {}, 'minimalRole': 'user'}, + 'command8': {'parameters': {}, 'minimalRole': 'user'} + } + })"; + EXPECT_JSON_EQ(expected, *json); } TEST(CommandDictionary, LoadWithPermissions) { @@ -131,51 +406,161 @@ 'base': { 'command1': { 'parameters': {}, - 'results': {} + 'results': {}, + 'visibility':'none' }, 'command2': { 'minimalRole': 'viewer', 'parameters': {}, - 'results': {} + 'results': {}, + 'visibility':'local' }, 'command3': { 'minimalRole': 'user', 'parameters': {}, - 'results': {} + 'results': {}, + 'visibility':'cloud' }, 'command4': { 'minimalRole': 'manager', 'parameters': {}, - 'results': {} + 'results': {}, + 'visibility':'all' }, 'command5': { 'minimalRole': 'owner', 'parameters': {}, + 'results': {}, + 'visibility':'local,cloud' + } + } + })"); + EXPECT_TRUE(base_dict.LoadCommands(*json, nullptr, nullptr)); + + auto cmd = base_dict.FindCommand("base.command1"); + ASSERT_NE(nullptr, cmd); + EXPECT_EQ("none", cmd->GetVisibility().ToString()); + EXPECT_EQ(UserRole::kUser, cmd->GetMinimalRole()); + + cmd = base_dict.FindCommand("base.command2"); + ASSERT_NE(nullptr, cmd); + EXPECT_EQ("local", cmd->GetVisibility().ToString()); + EXPECT_EQ(UserRole::kViewer, cmd->GetMinimalRole()); + + cmd = base_dict.FindCommand("base.command3"); + ASSERT_NE(nullptr, cmd); + EXPECT_EQ("cloud", cmd->GetVisibility().ToString()); + EXPECT_EQ(UserRole::kUser, cmd->GetMinimalRole()); + + cmd = base_dict.FindCommand("base.command4"); + ASSERT_NE(nullptr, cmd); + EXPECT_EQ("all", cmd->GetVisibility().ToString()); + EXPECT_EQ(UserRole::kManager, cmd->GetMinimalRole()); + + cmd = base_dict.FindCommand("base.command5"); + ASSERT_NE(nullptr, cmd); + EXPECT_EQ("all", cmd->GetVisibility().ToString()); + EXPECT_EQ(UserRole::kOwner, cmd->GetMinimalRole()); + + CommandDictionary dict; + json = CreateDictionaryValue(R"({ + 'base': { + 'command1': { + 'parameters': {}, + 'results': {} + }, + 'command2': { + 'parameters': {}, + 'results': {} + }, + 'command3': { + 'parameters': {}, + 'results': {} + }, + 'command4': { + 'parameters': {}, + 'results': {} + }, + 'command5': { + 'parameters': {}, + 'results': {} + }, + '_command6': { + 'parameters': {}, 'results': {} } } })"); - EXPECT_TRUE(base_dict.LoadCommands(*json, nullptr)); + EXPECT_TRUE(dict.LoadCommands(*json, &base_dict, nullptr)); - auto cmd = base_dict.FindCommand("base.command1"); + cmd = dict.FindCommand("base.command1"); ASSERT_NE(nullptr, cmd); + EXPECT_EQ("none", cmd->GetVisibility().ToString()); EXPECT_EQ(UserRole::kUser, cmd->GetMinimalRole()); - cmd = base_dict.FindCommand("base.command2"); + cmd = dict.FindCommand("base.command2"); ASSERT_NE(nullptr, cmd); + EXPECT_EQ("local", cmd->GetVisibility().ToString()); EXPECT_EQ(UserRole::kViewer, cmd->GetMinimalRole()); - cmd = base_dict.FindCommand("base.command3"); + cmd = dict.FindCommand("base.command3"); ASSERT_NE(nullptr, cmd); + EXPECT_EQ("cloud", cmd->GetVisibility().ToString()); EXPECT_EQ(UserRole::kUser, cmd->GetMinimalRole()); - cmd = base_dict.FindCommand("base.command4"); + cmd = dict.FindCommand("base.command4"); ASSERT_NE(nullptr, cmd); + EXPECT_EQ("all", cmd->GetVisibility().ToString()); EXPECT_EQ(UserRole::kManager, cmd->GetMinimalRole()); - cmd = base_dict.FindCommand("base.command5"); + cmd = dict.FindCommand("base.command5"); ASSERT_NE(nullptr, cmd); + EXPECT_EQ("all", cmd->GetVisibility().ToString()); EXPECT_EQ(UserRole::kOwner, cmd->GetMinimalRole()); + + cmd = dict.FindCommand("base._command6"); + ASSERT_NE(nullptr, cmd); + EXPECT_EQ("all", cmd->GetVisibility().ToString()); + EXPECT_EQ(UserRole::kUser, cmd->GetMinimalRole()); +} + +TEST(CommandDictionary, LoadWithPermissions_InvalidVisibility) { + CommandDictionary dict; + ErrorPtr error; + + auto json = CreateDictionaryValue(R"({ + 'base': { + 'jump': { + 'parameters': {}, + 'results': {}, + 'visibility':'foo' + } + } + })"); + EXPECT_FALSE(dict.LoadCommands(*json, nullptr, &error)); + EXPECT_EQ("invalid_command_visibility", error->GetCode()); + EXPECT_EQ("invalid_parameter_value", error->GetInnerError()->GetCode()); + error.reset(); +} + +TEST(CommandDictionary, LoadWithPermissions_InvalidRole) { + CommandDictionary dict; + ErrorPtr error; + + auto json = CreateDictionaryValue(R"({ + 'base': { + 'jump': { + 'parameters': {}, + 'results': {}, + 'visibility':'local,cloud', + 'minimalRole':'foo' + } + } + })"); + EXPECT_FALSE(dict.LoadCommands(*json, nullptr, &error)); + EXPECT_EQ("invalid_minimal_role", error->GetCode()); + EXPECT_EQ("invalid_parameter_value", error->GetInnerError()->GetCode()); + error.reset(); } } // namespace weave
diff --git a/src/commands/command_instance.cc b/src/commands/command_instance.cc index 8328299..f152d82 100644 --- a/src/commands/command_instance.cc +++ b/src/commands/command_instance.cc
@@ -12,7 +12,9 @@ #include "src/commands/command_definition.h" #include "src/commands/command_dictionary.h" #include "src/commands/command_queue.h" +#include "src/commands/prop_types.h" #include "src/commands/schema_constants.h" +#include "src/commands/schema_utils.h" #include "src/json_error_codes.h" #include "src/utils.h" @@ -66,12 +68,12 @@ CommandInstance::CommandInstance(const std::string& name, Command::Origin origin, const CommandDefinition* command_definition, - const base::DictionaryValue& parameters) + const ValueMap& parameters) : name_{name}, origin_{origin}, - command_definition_{command_definition} { + command_definition_{command_definition}, + parameters_{parameters} { CHECK(command_definition_); - parameters_.MergeDictionary(¶meters); } CommandInstance::~CommandInstance() { @@ -94,16 +96,16 @@ return origin_; } -const base::DictionaryValue& CommandInstance::GetParameters() const { - return parameters_; +std::unique_ptr<base::DictionaryValue> CommandInstance::GetParameters() const { + return TypedValueToJson(parameters_); } -const base::DictionaryValue& CommandInstance::GetProgress() const { - return progress_; +std::unique_ptr<base::DictionaryValue> CommandInstance::GetProgress() const { + return TypedValueToJson(progress_); } -const base::DictionaryValue& CommandInstance::GetResults() const { - return results_; +std::unique_ptr<base::DictionaryValue> CommandInstance::GetResults() const { + return TypedValueToJson(results_); } const Error* CommandInstance::GetError() const { @@ -112,13 +114,21 @@ bool CommandInstance::SetProgress(const base::DictionaryValue& progress, ErrorPtr* error) { + if (!command_definition_) + return ReportDestroyedError(error); + ObjectPropType obj_prop_type; + obj_prop_type.SetObjectSchema(command_definition_->GetProgress()->Clone()); + + ValueMap obj; + if (!TypedValueFromJson(&progress, &obj_prop_type, &obj, error)) + return false; + // Change status even if progress unchanged, e.g. 0% -> 0%. if (!SetStatus(State::kInProgress, error)) return false; - if (!progress_.Equals(&progress)) { - progress_.Clear(); - progress_.MergeDictionary(&progress); + if (obj != progress_) { + progress_ = obj; FOR_EACH_OBSERVER(Observer, observers_, OnProgressChanged()); } @@ -127,9 +137,17 @@ bool CommandInstance::Complete(const base::DictionaryValue& results, ErrorPtr* error) { - if (!results_.Equals(&results)) { - results_.Clear(); - results_.MergeDictionary(&results); + if (!command_definition_) + return ReportDestroyedError(error); + ObjectPropType obj_prop_type; + obj_prop_type.SetObjectSchema(command_definition_->GetResults()->Clone()); + + ValueMap obj; + if (!TypedValueFromJson(&results, &obj_prop_type, &obj, error)) + return false; + + if (obj != results_) { + results_ = obj; FOR_EACH_OBSERVER(Observer, observers_, OnResultsChanged()); } // Change status even if result is unchanged. @@ -153,29 +171,36 @@ // On success, returns |true| and the validated parameters and values through // |parameters|. Otherwise returns |false| and additional error information in // |error|. -std::unique_ptr<base::DictionaryValue> GetCommandParameters( - const base::DictionaryValue* json, - const CommandDefinition* command_def, - ErrorPtr* error) { +bool GetCommandParameters(const base::DictionaryValue* json, + const CommandDefinition* command_def, + ValueMap* parameters, + ErrorPtr* error) { // Get the command parameters from 'parameters' property. - std::unique_ptr<base::DictionaryValue> params; + base::DictionaryValue no_params; // Placeholder when no params are specified. + const base::DictionaryValue* params = nullptr; const base::Value* params_value = nullptr; if (json->Get(commands::attributes::kCommand_Parameters, ¶ms_value)) { // Make sure the "parameters" property is actually an object. - const base::DictionaryValue* params_dict = nullptr; - if (!params_value->GetAsDictionary(¶ms_dict)) { + if (!params_value->GetAsDictionary(¶ms)) { Error::AddToPrintf(error, FROM_HERE, errors::json::kDomain, errors::json::kObjectExpected, "Property '%s' must be a JSON object", commands::attributes::kCommand_Parameters); - return params; + return false; } - params.reset(params_dict->DeepCopy()); } else { // "parameters" are not specified. Assume empty param list. - params.reset(new base::DictionaryValue); + params = &no_params; } - return params; + + // Now read in the parameters and validate their values against the command + // definition schema. + ObjectPropType obj_prop_type; + obj_prop_type.SetObjectSchema(command_def->GetParameters()->Clone()); + if (!TypedValueFromJson(params, &obj_prop_type, parameters, error)) { + return false; + } + return true; } } // anonymous namespace @@ -221,8 +246,8 @@ return instance; } - auto parameters = GetCommandParameters(json, command_def, error); - if (!parameters) { + ValueMap parameters; + if (!GetCommandParameters(json, command_def, ¶meters, error)) { Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, errors::commands::kCommandFailed, "Failed to validate command '%s'", command_name.c_str()); @@ -230,7 +255,7 @@ } instance.reset( - new CommandInstance{command_name, origin, command_def, *parameters}); + new CommandInstance{command_name, origin, command_def, parameters}); if (!command_id->empty()) instance->SetID(*command_id); @@ -243,9 +268,12 @@ json->SetString(commands::attributes::kCommand_Id, id_); json->SetString(commands::attributes::kCommand_Name, name_); - json->Set(commands::attributes::kCommand_Parameters, parameters_.DeepCopy()); - json->Set(commands::attributes::kCommand_Progress, progress_.DeepCopy()); - json->Set(commands::attributes::kCommand_Results, results_.DeepCopy()); + json->Set(commands::attributes::kCommand_Parameters, + TypedValueToJson(parameters_).release()); + json->Set(commands::attributes::kCommand_Progress, + TypedValueToJson(progress_).release()); + json->Set(commands::attributes::kCommand_Results, + TypedValueToJson(results_).release()); json->SetString(commands::attributes::kCommand_State, EnumToString(state_)); if (error_) { json->Set(commands::attributes::kCommand_Error,
diff --git a/src/commands/command_instance.h b/src/commands/command_instance.h index 32a93a9..5a2ebf7 100644 --- a/src/commands/command_instance.h +++ b/src/commands/command_instance.h
@@ -15,6 +15,9 @@ #include <weave/error.h> #include <weave/command.h> +#include "src/commands/prop_values.h" +#include "src/commands/schema_utils.h" + namespace base { class Value; } // namespace base @@ -37,16 +40,16 @@ virtual void OnStateChanged() = 0; protected: - virtual ~Observer() {} + virtual ~Observer() = default; }; // Construct a command instance given the full command |name| which must - // be in format "<package_name>.<command_name>" and a list of parameters and - // their values specified in |parameters|. + // be in format "<package_name>.<command_name>", a command |category| and + // a list of parameters and their values specified in |parameters|. CommandInstance(const std::string& name, Command::Origin origin, const CommandDefinition* command_definition, - const base::DictionaryValue& parameters); + const ValueMap& parameters); ~CommandInstance() override; // Command overrides. @@ -54,9 +57,9 @@ const std::string& GetName() const override; Command::State GetState() const override; Command::Origin GetOrigin() const override; - const base::DictionaryValue& GetParameters() const override; - const base::DictionaryValue& GetProgress() const override; - const base::DictionaryValue& GetResults() const override; + std::unique_ptr<base::DictionaryValue> GetParameters() const override; + std::unique_ptr<base::DictionaryValue> GetProgress() const override; + std::unique_ptr<base::DictionaryValue> GetResults() const override; const Error* GetError() const override; bool SetProgress(const base::DictionaryValue& progress, ErrorPtr* error) override; @@ -121,11 +124,11 @@ // Command definition. const CommandDefinition* command_definition_{nullptr}; // Command parameters and their values. - base::DictionaryValue parameters_; + ValueMap parameters_; // Current command execution progress. - base::DictionaryValue progress_; + ValueMap progress_; // Command results. - base::DictionaryValue results_; + ValueMap results_; // Current command state. Command::State state_ = Command::State::kQueued; // Error encountered during execution of the command.
diff --git a/src/commands/command_instance_unittest.cc b/src/commands/command_instance_unittest.cc index fb8fe84..4e208ed 100644 --- a/src/commands/command_instance_unittest.cc +++ b/src/commands/command_instance_unittest.cc
@@ -5,9 +5,11 @@ #include "src/commands/command_instance.h" #include <gtest/gtest.h> -#include <weave/test/unittest_utils.h> #include "src/commands/command_dictionary.h" +#include "src/commands/prop_types.h" +#include "src/commands/schema_utils.h" +#include "src/commands/unittest_utils.h" namespace weave { @@ -59,7 +61,7 @@ } } })"); - CHECK(dict_.LoadCommands(*json, nullptr)) + CHECK(dict_.LoadCommands(*json, nullptr, nullptr)) << "Failed to parse test command dictionary"; } CommandDictionary dict_; @@ -68,12 +70,14 @@ } // anonymous namespace TEST_F(CommandInstanceTest, Test) { - auto params = CreateDictionaryValue(R"({ - 'phrase': 'iPityDaFool', - 'volume': 5 - })"); + StringPropType str_prop; + IntPropType int_prop; + ValueMap params; + params["phrase"] = + str_prop.CreateValue(base::StringValue{"iPityDaFool"}, nullptr); + params["volume"] = int_prop.CreateValue(base::FundamentalValue{5}, nullptr); CommandInstance instance{"robot.speak", Command::Origin::kCloud, - dict_.FindCommand("robot.speak"), *params}; + dict_.FindCommand("robot.speak"), params}; EXPECT_TRUE( instance.Complete(*CreateDictionaryValue("{'foo': 239}"), nullptr)); @@ -82,8 +86,8 @@ EXPECT_EQ("robot.speak", instance.GetName()); EXPECT_EQ(Command::Origin::kCloud, instance.GetOrigin()); EXPECT_JSON_EQ("{'phrase': 'iPityDaFool', 'volume': 5}", - instance.GetParameters()); - EXPECT_JSON_EQ("{'foo': 239}", instance.GetResults()); + *instance.GetParameters()); + EXPECT_JSON_EQ("{'foo': 239}", *instance.GetResults()); CommandInstance instance2{"base.reboot", Command::Origin::kLocal, @@ -118,7 +122,7 @@ EXPECT_EQ("abcd", instance->GetID()); EXPECT_EQ("robot.jump", instance->GetName()); EXPECT_JSON_EQ("{'height': 53, '_jumpType': '_withKick'}", - instance->GetParameters()); + *instance->GetParameters()); } TEST_F(CommandInstanceTest, FromJson_ParamsOmitted) { @@ -126,7 +130,7 @@ auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud, dict_, nullptr, nullptr); EXPECT_EQ("base.reboot", instance->GetName()); - EXPECT_JSON_EQ("{}", instance->GetParameters()); + EXPECT_JSON_EQ("{}", *instance->GetParameters()); } TEST_F(CommandInstanceTest, FromJson_NotObject) { @@ -170,6 +174,25 @@ EXPECT_EQ("command_failed", error->GetCode()); } +TEST_F(CommandInstanceTest, FromJson_ParamError) { + auto json = CreateDictionaryValue(R"({ + 'name': 'robot.speak', + 'parameters': { + 'phrase': 'iPityDaFool', + 'volume': 20 + } + })"); + ErrorPtr error; + auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud, + dict_, nullptr, &error); + EXPECT_EQ(nullptr, instance.get()); + auto first = error->GetFirstError(); + EXPECT_EQ("out_of_range", first->GetCode()); + auto inner = error->GetInnerError(); + EXPECT_EQ("invalid_parameter_value", inner->GetCode()); + EXPECT_EQ("command_failed", error->GetCode()); +} + TEST_F(CommandInstanceTest, ToJson) { auto json = CreateDictionaryValue(R"({ 'name': 'robot.jump',
diff --git a/src/commands/command_manager.cc b/src/commands/command_manager.cc index a64c8e5..75d8295 100644 --- a/src/commands/command_manager.cc +++ b/src/commands/command_manager.cc
@@ -28,7 +28,7 @@ bool CommandManager::LoadCommands(const base::DictionaryValue& dict, ErrorPtr* error) { - bool result = dictionary_.LoadCommands(dict, error); + bool result = dictionary_.LoadCommands(dict, nullptr, error); for (const auto& cb : on_command_changed_) cb.Run(); return result; @@ -83,6 +83,37 @@ return command_queue_.Find(id); } +bool CommandManager::SetCommandVisibility( + const std::vector<std::string>& command_names, + CommandDefinition::Visibility visibility, + ErrorPtr* error) { + if (command_names.empty()) + return true; + + std::vector<CommandDefinition*> definitions; + definitions.reserve(command_names.size()); + + // Find/validate command definitions first. + for (const std::string& name : command_names) { + CommandDefinition* def = dictionary_.FindCommand(name); + if (!def) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidCommandName, + "Command '%s' is unknown", name.c_str()); + return false; + } + definitions.push_back(def); + } + + // Now that we know that all the command names were valid, + // update the respective commands' visibility. + for (CommandDefinition* def : definitions) + def->SetVisibility(visibility); + for (const auto& cb : on_command_changed_) + cb.Run(); + return true; +} + void CommandManager::AddCommandAddedCallback( const CommandQueue::CommandCallback& callback) { command_queue_.AddCommandAddedCallback(callback);
diff --git a/src/commands/command_manager.h b/src/commands/command_manager.h index 04f49ee..7cc8907 100644 --- a/src/commands/command_manager.h +++ b/src/commands/command_manager.h
@@ -60,6 +60,11 @@ // Adds a new command to the command queue. void AddCommand(std::unique_ptr<CommandInstance> command_instance); + // Changes the visibility of commands. + bool SetCommandVisibility(const std::vector<std::string>& command_names, + CommandDefinition::Visibility visibility, + ErrorPtr* error); + bool AddCommand(const base::DictionaryValue& command, UserRole role, std::string* id,
diff --git a/src/commands/command_manager_unittest.cc b/src/commands/command_manager_unittest.cc index f0dc95d..0c890a9 100644 --- a/src/commands/command_manager_unittest.cc +++ b/src/commands/command_manager_unittest.cc
@@ -9,9 +9,9 @@ #include <base/json/json_writer.h> #include <gtest/gtest.h> #include <weave/provider/test/mock_config_store.h> -#include <weave/test/unittest_utils.h> #include "src/bind_lambda.h" +#include "src/commands/unittest_utils.h" using testing::Return; @@ -75,7 +75,7 @@ } })"; EXPECT_TRUE(manager.LoadCommands(json_str, nullptr)); - EXPECT_EQ(2u, manager.GetCommandDictionary().GetSize()); + EXPECT_EQ(2, manager.GetCommandDictionary().GetSize()); EXPECT_NE(nullptr, manager.GetCommandDictionary().FindCommand("base.reboot")); EXPECT_NE(nullptr, manager.GetCommandDictionary().FindCommand("robot._jump")); } @@ -84,11 +84,66 @@ CommandManager manager; ASSERT_TRUE(manager.LoadCommands(kTestVendorCommands, nullptr)); ASSERT_TRUE(manager.LoadCommands(kTestTestCommands, nullptr)); - EXPECT_EQ(3u, manager.GetCommandDictionary().GetSize()); + EXPECT_EQ(3, manager.GetCommandDictionary().GetSize()); EXPECT_NE(nullptr, manager.GetCommandDictionary().FindCommand("robot._jump")); EXPECT_NE(nullptr, manager.GetCommandDictionary().FindCommand("robot._speak")); EXPECT_NE(nullptr, manager.GetCommandDictionary().FindCommand("test._yo")); } +TEST(CommandManager, UpdateCommandVisibility) { + CommandManager manager; + int update_count = 0; + auto on_command_change = [&update_count]() { update_count++; }; + manager.AddCommandDefChanged(base::Bind(on_command_change)); + + auto json = CreateDictionaryValue(R"({ + 'foo': { + '_baz': { + 'parameters': {}, + 'results': {} + }, + '_bar': { + 'parameters': {}, + 'results': {} + } + }, + 'bar': { + '_quux': { + 'parameters': {}, + 'results': {}, + 'visibility': 'none' + } + } + })"); + ASSERT_TRUE(manager.LoadCommands(*json, nullptr)); + EXPECT_EQ(2, update_count); + const CommandDictionary& dict = manager.GetCommandDictionary(); + EXPECT_TRUE(manager.SetCommandVisibility( + {"foo._baz"}, CommandDefinition::Visibility::GetLocal(), nullptr)); + EXPECT_EQ(3, update_count); + EXPECT_EQ("local", dict.FindCommand("foo._baz")->GetVisibility().ToString()); + EXPECT_EQ("all", dict.FindCommand("foo._bar")->GetVisibility().ToString()); + EXPECT_EQ("none", dict.FindCommand("bar._quux")->GetVisibility().ToString()); + + ErrorPtr error; + ASSERT_FALSE(manager.SetCommandVisibility( + {"foo._baz", "foo._bar", "test.cmd"}, + CommandDefinition::Visibility::GetLocal(), &error)); + EXPECT_EQ(errors::commands::kInvalidCommandName, error->GetCode()); + // The visibility state of commands shouldn't have changed. + EXPECT_EQ(3, update_count); + EXPECT_EQ("local", dict.FindCommand("foo._baz")->GetVisibility().ToString()); + EXPECT_EQ("all", dict.FindCommand("foo._bar")->GetVisibility().ToString()); + EXPECT_EQ("none", dict.FindCommand("bar._quux")->GetVisibility().ToString()); + + EXPECT_TRUE(manager.SetCommandVisibility( + {"foo._baz", "bar._quux"}, CommandDefinition::Visibility::GetCloud(), + nullptr)); + EXPECT_EQ(4, update_count); + EXPECT_EQ("cloud", dict.FindCommand("foo._baz")->GetVisibility().ToString()); + EXPECT_EQ("all", dict.FindCommand("foo._bar")->GetVisibility().ToString()); + EXPECT_EQ("cloud", dict.FindCommand("bar._quux")->GetVisibility().ToString()); +} + } // namespace weave
diff --git a/src/commands/command_queue.cc b/src/commands/command_queue.cc index 3d2167f..e08527b 100644 --- a/src/commands/command_queue.cc +++ b/src/commands/command_queue.cc
@@ -38,8 +38,7 @@ } } - CHECK(command_callbacks_.insert(std::make_pair(command_name, callback)) - .second) + CHECK(command_callbacks_.emplace(command_name, callback).second) << command_name << " already has handler"; } else {
diff --git a/src/commands/command_queue_unittest.cc b/src/commands/command_queue_unittest.cc index dc7290a..5060071 100644 --- a/src/commands/command_queue_unittest.cc +++ b/src/commands/command_queue_unittest.cc
@@ -13,21 +13,18 @@ #include <gtest/gtest.h> #include "src/commands/command_definition.h" +#include "src/commands/object_schema.h" #include "src/string_utils.h" namespace weave { class CommandQueueTest : public testing::Test { public: - CommandQueueTest() { - command_definition_ = CommandDefinition::FromJson({}, nullptr); - } - std::unique_ptr<CommandInstance> CreateDummyCommandInstance( const std::string& name, const std::string& id) { std::unique_ptr<CommandInstance> cmd{new CommandInstance{ - name, Command::Origin::kLocal, command_definition_.get(), {}}}; + name, Command::Origin::kLocal, &command_definition_, {}}}; cmd->SetID(id); return cmd; } @@ -42,7 +39,8 @@ CommandQueue queue_; private: - std::unique_ptr<CommandDefinition> command_definition_; + CommandDefinition command_definition_{ + ObjectSchema::Create(), ObjectSchema::Create(), ObjectSchema::Create()}; }; // Keeps track of commands being added to and removed from the queue_. @@ -83,14 +81,14 @@ TEST_F(CommandQueueTest, Empty) { EXPECT_TRUE(queue_.IsEmpty()); - EXPECT_EQ(0u, queue_.GetCount()); + EXPECT_EQ(0, queue_.GetCount()); } TEST_F(CommandQueueTest, Add) { queue_.Add(CreateDummyCommandInstance("base.reboot", "id1")); queue_.Add(CreateDummyCommandInstance("base.reboot", "id2")); queue_.Add(CreateDummyCommandInstance("base.reboot", "id3")); - EXPECT_EQ(3u, queue_.GetCount()); + EXPECT_EQ(3, queue_.GetCount()); EXPECT_FALSE(queue_.IsEmpty()); } @@ -101,31 +99,31 @@ queue_.Add(CreateDummyCommandInstance("base.reboot", id2)); EXPECT_FALSE(queue_.IsEmpty()); EXPECT_FALSE(Remove("dummy")); - EXPECT_EQ(2u, queue_.GetCount()); + EXPECT_EQ(2, queue_.GetCount()); EXPECT_TRUE(Remove(id1)); - EXPECT_EQ(1u, queue_.GetCount()); + EXPECT_EQ(1, queue_.GetCount()); EXPECT_FALSE(Remove(id1)); - EXPECT_EQ(1u, queue_.GetCount()); + EXPECT_EQ(1, queue_.GetCount()); EXPECT_TRUE(Remove(id2)); - EXPECT_EQ(0u, queue_.GetCount()); + EXPECT_EQ(0, queue_.GetCount()); EXPECT_FALSE(Remove(id2)); - EXPECT_EQ(0u, queue_.GetCount()); + EXPECT_EQ(0, queue_.GetCount()); EXPECT_TRUE(queue_.IsEmpty()); } TEST_F(CommandQueueTest, DelayedRemove) { const std::string id1 = "id1"; queue_.Add(CreateDummyCommandInstance("base.reboot", id1)); - EXPECT_EQ(1u, queue_.GetCount()); + EXPECT_EQ(1, queue_.GetCount()); queue_.DelayedRemove(id1); - EXPECT_EQ(1u, queue_.GetCount()); + EXPECT_EQ(1, queue_.GetCount()); Cleanup(base::TimeDelta::FromMinutes(1)); - EXPECT_EQ(1u, queue_.GetCount()); + EXPECT_EQ(1, queue_.GetCount()); Cleanup(base::TimeDelta::FromMinutes(15)); - EXPECT_EQ(0u, queue_.GetCount()); + EXPECT_EQ(0, queue_.GetCount()); } TEST_F(CommandQueueTest, Dispatch) {
diff --git a/src/commands/object_schema.cc b/src/commands/object_schema.cc new file mode 100644 index 0000000..b70beff --- /dev/null +++ b/src/commands/object_schema.cc
@@ -0,0 +1,378 @@ +// 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
diff --git a/src/commands/object_schema.h b/src/commands/object_schema.h new file mode 100644 index 0000000..504e1dd --- /dev/null +++ b/src/commands/object_schema.h
@@ -0,0 +1,95 @@ +// 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. + +#ifndef LIBWEAVE_SRC_COMMANDS_OBJECT_SCHEMA_H_ +#define LIBWEAVE_SRC_COMMANDS_OBJECT_SCHEMA_H_ + +#include <map> +#include <memory> +#include <string> + +#include <weave/error.h> + +namespace base { +class Value; +class DictionaryValue; +} // namespace base + +namespace weave { + +class PropType; + +// ObjectSchema is a class representing an object definition in GCD command +// schema. This could represent a GCD command definition, but also it can be +// used when defining custom object types for command properties such as +// output media type (paper) for print command. The schema definition for +// these type of object description is the same. +class ObjectSchema final { + public: + // Do not inline the constructor/destructor to allow forward-declared type + // PropType to be part of |properties_| member. + ObjectSchema(); + ~ObjectSchema(); + + // Properties is a string-to-PropType map representing a list of + // properties defined for a command/object. The key is the parameter + // name and the value is the parameter type definition object. + using Properties = std::map<std::string, std::unique_ptr<PropType>>; + + // Makes a full copy of this object. + virtual std::unique_ptr<ObjectSchema> Clone() const; + + // Add a new parameter definition. + void AddProp(const std::string& name, std::unique_ptr<PropType> prop); + + // Finds parameter type definition by name. Returns nullptr if not found. + const PropType* GetProp(const std::string& name) const; + + // Gets the list of all the properties defined. + const Properties& GetProps() const { return properties_; } + + // Marks the property with given name as "required". If |name| specifies + // an unknown property, false is returned and |error| is set with detailed + // error message for the failure. + bool MarkPropRequired(const std::string& name, ErrorPtr* error); + + // Specify whether extra properties are allowed on objects described by + // this schema. When validating a value of an object type, we can + // make sure that the value has only the properties explicitly defined by + // the schema and no other (custom) properties are allowed. + // This is to support JSON Schema's "additionalProperties" specification. + bool GetExtraPropertiesAllowed() const { return extra_properties_allowed_; } + void SetExtraPropertiesAllowed(bool allowed) { + extra_properties_allowed_ = allowed; + } + + // Saves the object schema to JSON. When |full_schema| is set to true, + // then all properties and constraints are saved, otherwise, only + // the overridden (not inherited) ones are saved. + std::unique_ptr<base::DictionaryValue> ToJson(bool full_schema, + bool in_command_def) const; + + // Loads the object schema from JSON. If |object_schema| is not nullptr, it is + // used as a base schema to inherit omitted properties and constraints from. + bool FromJson(const base::DictionaryValue* value, + const ObjectSchema* object_schema, + ErrorPtr* error); + + // Helper factory method to create a new instance of ObjectSchema object. + static std::unique_ptr<ObjectSchema> Create(); + + // Helper method to load property type definitions from JSON. + static std::unique_ptr<PropType> PropFromJson(const base::Value& value, + const PropType* base_schema, + ErrorPtr* error); + + private: + // Internal parameter type definition map. + Properties properties_; + bool extra_properties_allowed_{false}; +}; + +} // namespace weave + +#endif // LIBWEAVE_SRC_COMMANDS_OBJECT_SCHEMA_H_
diff --git a/src/commands/object_schema_unittest.cc b/src/commands/object_schema_unittest.cc new file mode 100644 index 0000000..6417952 --- /dev/null +++ b/src/commands/object_schema_unittest.cc
@@ -0,0 +1,1697 @@ +// 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 <memory> +#include <vector> + +#include <base/json/json_reader.h> +#include <base/json/json_writer.h> +#include <base/values.h> +#include <gtest/gtest.h> + +#include "src/commands/prop_constraints.h" +#include "src/commands/prop_types.h" +#include "src/commands/schema_constants.h" +#include "src/commands/unittest_utils.h" + +namespace weave { + +using test::CreateValue; +using test::CreateDictionaryValue; + +namespace { + +template <typename T> +std::vector<T> GetArrayValues(const ValueVector& arr) { + std::vector<T> values; + values.reserve(arr.size()); + for (const auto& prop_value : arr) { + const auto& value = static_cast<const TypedValueBase<T>&>(*prop_value); + values.push_back(value.GetValue()); + } + return values; +} + +template <typename T> +std::vector<T> GetOneOfValues(const PropType* prop_type) { + auto one_of = static_cast<const ConstraintOneOf*>( + prop_type->GetConstraint(ConstraintType::OneOf)); + if (!one_of) + return {}; + + return GetArrayValues<T>(one_of->set_.value); +} + +bool ValidateValue(const PropType& type, + const base::Value& value, + ErrorPtr* error) { + std::unique_ptr<PropValue> val = type.CreatePropValue(value, error); + return val != nullptr; +} + +} // anonymous namespace + +TEST(CommandSchema, IntPropType_Empty) { + IntPropType prop; + EXPECT_TRUE(prop.GetConstraints().empty()); + EXPECT_FALSE(prop.HasOverriddenAttributes()); + EXPECT_FALSE(prop.IsBasedOnSchema()); + EXPECT_EQ(nullptr, prop.GetDefaultValue()); +} + +TEST(CommandSchema, IntPropType_Types) { + IntPropType prop; + EXPECT_EQ(nullptr, prop.GetBoolean()); + EXPECT_EQ(&prop, prop.GetInt()); + EXPECT_EQ(nullptr, prop.GetDouble()); + EXPECT_EQ(nullptr, prop.GetString()); + EXPECT_EQ(nullptr, prop.GetObject()); + EXPECT_EQ(nullptr, prop.GetArray()); +} + +TEST(CommandSchema, IntPropType_ToJson) { + IntPropType prop; + EXPECT_JSON_EQ("'integer'", *prop.ToJson(false, false)); + EXPECT_JSON_EQ("{'type':'integer'}", *prop.ToJson(true, false)); + IntPropType param2; + param2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_JSON_EQ("{}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'minimum':3}").get(), &prop, nullptr); + EXPECT_JSON_EQ("{'minimum':3}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'maximum':-7}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("{'maximum':-7}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'minimum':0,'maximum':5}").get(), + &prop, nullptr); + EXPECT_JSON_EQ("{'maximum':5,'minimum':0}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'enum':[1,2,3]}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("[1,2,3]", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'default':123}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("{'default':123}", *param2.ToJson(false, false)); +} + +TEST(CommandSchema, IntPropType_FromJson) { + IntPropType prop; + prop.AddMinMaxConstraint(2, 8); + IntPropType param2; + param2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_FALSE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_EQ(2, prop.GetMinValue()); + EXPECT_EQ(8, prop.GetMaxValue()); + prop.AddMinMaxConstraint(-2, 30); + param2.FromJson(CreateDictionaryValue("{'minimum':7}").get(), &prop, nullptr); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_EQ(7, param2.GetMinValue()); + EXPECT_EQ(30, param2.GetMaxValue()); + param2.FromJson(CreateDictionaryValue("{'maximum':17}").get(), &prop, + nullptr); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_EQ(-2, param2.GetMinValue()); + EXPECT_EQ(17, param2.GetMaxValue()); + + ASSERT_TRUE(param2.FromJson(CreateDictionaryValue("{'default':3}").get(), + &prop, nullptr)); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + ASSERT_NE(nullptr, param2.GetDefaultValue()); + EXPECT_EQ(3, param2.GetDefaultValue()->GetInt()->GetValue()); +} + +TEST(CommandSchema, IntPropType_Validate) { + IntPropType prop; + prop.AddMinMaxConstraint(2, 4); + ErrorPtr error; + EXPECT_FALSE(ValidateValue(prop, *CreateValue("-1"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("0"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("1"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("2"), &error)); + EXPECT_EQ(nullptr, error.get()); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("3"), &error)); + EXPECT_EQ(nullptr, error.get()); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("4"), &error)); + EXPECT_EQ(nullptr, error.get()); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("5"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("true"), &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("3.0"), &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("'3'"), &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); +} + +TEST(CommandSchema, IntPropType_CreateValue) { + IntPropType prop; + ErrorPtr error; + auto val = prop.CreateValue(base::FundamentalValue{2}, &error); + ASSERT_NE(nullptr, val.get()); + EXPECT_EQ(nullptr, error.get()); + EXPECT_EQ(2, val->GetValue()); + + val = prop.CreateValue(base::StringValue{"blah"}, &error); + EXPECT_EQ(nullptr, val.get()); + ASSERT_NE(nullptr, error.get()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); +} + +/////////////////////////////////////////////////////////////////////////////// + +TEST(CommandSchema, BoolPropType_Empty) { + BooleanPropType prop; + EXPECT_TRUE(prop.GetConstraints().empty()); + EXPECT_FALSE(prop.HasOverriddenAttributes()); + EXPECT_FALSE(prop.IsBasedOnSchema()); + EXPECT_EQ(nullptr, prop.GetDefaultValue()); + EXPECT_FALSE(prop.IsRequired()); +} + +TEST(CommandSchema, BoolPropType_Types) { + BooleanPropType prop; + EXPECT_EQ(nullptr, prop.GetInt()); + EXPECT_EQ(&prop, prop.GetBoolean()); + EXPECT_EQ(nullptr, prop.GetDouble()); + EXPECT_EQ(nullptr, prop.GetString()); + EXPECT_EQ(nullptr, prop.GetObject()); + EXPECT_EQ(nullptr, prop.GetArray()); +} + +TEST(CommandSchema, BoolPropType_ToJson) { + BooleanPropType prop; + EXPECT_JSON_EQ("'boolean'", *prop.ToJson(false, false)); + EXPECT_JSON_EQ("{'type':'boolean'}", *prop.ToJson(true, false)); + BooleanPropType param2; + param2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_JSON_EQ("{}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'enum':[true,false]}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("[true,false]", *param2.ToJson(false, false)); + EXPECT_JSON_EQ("{'enum':[true,false],'type':'boolean'}", + *param2.ToJson(true, false)); + param2.FromJson(CreateDictionaryValue("{'default':true}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("{'default':true}", *param2.ToJson(false, false)); +} + +TEST(CommandSchema, BoolPropType_FromJson) { + BooleanPropType prop; + prop.FromJson(CreateDictionaryValue("{'enum':[true]}").get(), &prop, nullptr); + BooleanPropType param2; + param2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_FALSE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_EQ(std::vector<bool>{true}, GetOneOfValues<bool>(&prop)); + + BooleanPropType prop_base; + BooleanPropType param3; + ASSERT_TRUE(param3.FromJson(CreateDictionaryValue("{'default':false}").get(), + &prop_base, nullptr)); + EXPECT_TRUE(param3.HasOverriddenAttributes()); + ASSERT_NE(nullptr, param3.GetDefaultValue()); + EXPECT_FALSE(param3.GetDefaultValue()->GetBoolean()->GetValue()); +} + +TEST(CommandSchema, BoolPropType_Validate) { + BooleanPropType prop; + prop.FromJson(CreateDictionaryValue("{'enum':[true]}").get(), &prop, nullptr); + ErrorPtr error; + EXPECT_FALSE(ValidateValue(prop, *CreateValue("false"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("true"), &error)); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("1"), &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("3.0"), &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("'3'"), &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); +} + +TEST(CommandSchema, BoolPropType_CreateValue) { + BooleanPropType prop; + ErrorPtr error; + auto val = prop.CreateValue(base::FundamentalValue{true}, &error); + ASSERT_NE(nullptr, val.get()); + EXPECT_EQ(nullptr, error.get()); + EXPECT_TRUE(val->GetValue()); + + val = prop.CreateValue(base::StringValue{"blah"}, &error); + EXPECT_EQ(nullptr, val.get()); + ASSERT_NE(nullptr, error.get()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); +} + +/////////////////////////////////////////////////////////////////////////////// + +TEST(CommandSchema, DoublePropType_Empty) { + DoublePropType prop; + EXPECT_DOUBLE_EQ(std::numeric_limits<double>::lowest(), prop.GetMinValue()); + EXPECT_DOUBLE_EQ((std::numeric_limits<double>::max)(), prop.GetMaxValue()); + EXPECT_FALSE(prop.HasOverriddenAttributes()); + EXPECT_FALSE(prop.IsBasedOnSchema()); + EXPECT_EQ(nullptr, prop.GetDefaultValue()); + EXPECT_FALSE(prop.IsRequired()); +} + +TEST(CommandSchema, DoublePropType_Types) { + DoublePropType prop; + EXPECT_EQ(nullptr, prop.GetInt()); + EXPECT_EQ(nullptr, prop.GetBoolean()); + EXPECT_EQ(&prop, prop.GetDouble()); + EXPECT_EQ(nullptr, prop.GetString()); + EXPECT_EQ(nullptr, prop.GetObject()); + EXPECT_EQ(nullptr, prop.GetArray()); +} + +TEST(CommandSchema, DoublePropType_ToJson) { + DoublePropType prop; + EXPECT_JSON_EQ("'number'", *prop.ToJson(false, false)); + EXPECT_JSON_EQ("{'type':'number'}", *prop.ToJson(true, false)); + DoublePropType param2; + param2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_JSON_EQ("{}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'minimum':3}").get(), &prop, nullptr); + EXPECT_JSON_EQ("{'minimum':3.0}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'maximum':-7}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("{'maximum':-7.0}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'minimum':0,'maximum':5}").get(), + &prop, nullptr); + EXPECT_JSON_EQ("{'maximum':5.0,'minimum':0.0}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'default':12.3}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("{'default':12.3}", *param2.ToJson(false, false)); +} + +TEST(CommandSchema, DoublePropType_FromJson) { + DoublePropType prop; + prop.AddMinMaxConstraint(2.5, 8.7); + DoublePropType param2; + param2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_FALSE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_DOUBLE_EQ(2.5, prop.GetMinValue()); + EXPECT_DOUBLE_EQ(8.7, prop.GetMaxValue()); + prop.AddMinMaxConstraint(-2.2, 30.4); + param2.FromJson(CreateDictionaryValue("{'minimum':7}").get(), &prop, nullptr); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_DOUBLE_EQ(7.0, param2.GetMinValue()); + EXPECT_DOUBLE_EQ(30.4, param2.GetMaxValue()); + param2.FromJson(CreateDictionaryValue("{'maximum':17.2}").get(), &prop, + nullptr); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_DOUBLE_EQ(-2.2, param2.GetMinValue()); + EXPECT_DOUBLE_EQ(17.2, param2.GetMaxValue()); + param2.FromJson(CreateDictionaryValue("{'minimum':0,'maximum':6.1}").get(), + &prop, nullptr); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_DOUBLE_EQ(0.0, param2.GetMinValue()); + EXPECT_DOUBLE_EQ(6.1, param2.GetMaxValue()); + + ASSERT_TRUE(param2.FromJson(CreateDictionaryValue("{'default':-1.234}").get(), + &prop, nullptr)); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + ASSERT_NE(nullptr, param2.GetDefaultValue()); + EXPECT_DOUBLE_EQ(-1.234, param2.GetDefaultValue()->GetDouble()->GetValue()); +} + +TEST(CommandSchema, DoublePropType_Validate) { + DoublePropType prop; + prop.AddMinMaxConstraint(-1.2, 1.3); + ErrorPtr error; + EXPECT_FALSE(ValidateValue(prop, *CreateValue("-2"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("-1.3"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("-1.2"), &error)); + EXPECT_EQ(nullptr, error.get()); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("0.0"), &error)); + EXPECT_EQ(nullptr, error.get()); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("1.3"), &error)); + EXPECT_EQ(nullptr, error.get()); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("1.31"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("true"), &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("'0.0'"), &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); +} + +TEST(CommandSchema, DoublePropType_CreateValue) { + DoublePropType prop; + ErrorPtr error; + auto val = prop.CreateValue(base::FundamentalValue{2.0}, &error); + ASSERT_NE(nullptr, val.get()); + EXPECT_EQ(nullptr, error.get()); + EXPECT_DOUBLE_EQ(2.0, val->GetValue()); + + val = prop.CreateValue(base::StringValue{"blah"}, &error); + EXPECT_EQ(nullptr, val.get()); + ASSERT_NE(nullptr, error.get()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); +} + +/////////////////////////////////////////////////////////////////////////////// + +TEST(CommandSchema, StringPropType_Empty) { + StringPropType prop; + EXPECT_EQ(0, prop.GetMinLength()); + EXPECT_EQ((std::numeric_limits<int>::max)(), prop.GetMaxLength()); + EXPECT_FALSE(prop.HasOverriddenAttributes()); + EXPECT_FALSE(prop.IsBasedOnSchema()); + EXPECT_EQ(nullptr, prop.GetDefaultValue()); + EXPECT_FALSE(prop.IsRequired()); +} + +TEST(CommandSchema, StringPropType_Types) { + StringPropType prop; + EXPECT_EQ(nullptr, prop.GetInt()); + EXPECT_EQ(nullptr, prop.GetBoolean()); + EXPECT_EQ(nullptr, prop.GetDouble()); + EXPECT_EQ(&prop, prop.GetString()); + EXPECT_EQ(nullptr, prop.GetObject()); + EXPECT_EQ(nullptr, prop.GetArray()); +} + +TEST(CommandSchema, StringPropType_ToJson) { + StringPropType prop; + EXPECT_JSON_EQ("'string'", *prop.ToJson(false, false)); + EXPECT_JSON_EQ("{'type':'string'}", *prop.ToJson(true, false)); + StringPropType param2; + param2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_JSON_EQ("{}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'minLength':3}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("{'minLength':3}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'maxLength':7}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("{'maxLength':7}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'minLength':0,'maxLength':5}").get(), + &prop, nullptr); + EXPECT_JSON_EQ("{'maxLength':5,'minLength':0}", *param2.ToJson(false, false)); + param2.FromJson(CreateDictionaryValue("{'default':'abcd'}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("{'default':'abcd'}", *param2.ToJson(false, false)); +} + +TEST(CommandSchema, StringPropType_FromJson) { + StringPropType prop; + prop.AddLengthConstraint(2, 8); + StringPropType param2; + param2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_FALSE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_EQ(2, prop.GetMinLength()); + EXPECT_EQ(8, prop.GetMaxLength()); + prop.AddLengthConstraint(3, 5); + param2.FromJson(CreateDictionaryValue("{'minLength':4}").get(), &prop, + nullptr); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_EQ(4, param2.GetMinLength()); + EXPECT_EQ(5, param2.GetMaxLength()); + param2.FromJson(CreateDictionaryValue("{'maxLength':8}").get(), &prop, + nullptr); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_EQ(3, param2.GetMinLength()); + EXPECT_EQ(8, param2.GetMaxLength()); + param2.FromJson(CreateDictionaryValue("{'minLength':1,'maxLength':7}").get(), + &prop, nullptr); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + EXPECT_TRUE(param2.IsBasedOnSchema()); + EXPECT_EQ(1, param2.GetMinLength()); + EXPECT_EQ(7, param2.GetMaxLength()); + + ASSERT_TRUE(param2.FromJson(CreateDictionaryValue("{'default':'foo'}").get(), + &prop, nullptr)); + EXPECT_TRUE(param2.HasOverriddenAttributes()); + ASSERT_NE(nullptr, param2.GetDefaultValue()); + EXPECT_EQ("foo", param2.GetDefaultValue()->GetString()->GetValue()); +} + +TEST(CommandSchema, StringPropType_Validate) { + StringPropType prop; + prop.AddLengthConstraint(1, 3); + ErrorPtr error; + EXPECT_FALSE(ValidateValue(prop, *CreateValue("''"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + prop.AddLengthConstraint(2, 3); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("''"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("'a'"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("'ab'"), &error)); + EXPECT_EQ(nullptr, error.get()); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("'abc'"), &error)); + EXPECT_EQ(nullptr, error.get()); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("'abcd'"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + + prop.FromJson(CreateDictionaryValue("{'enum':['abc','def','xyz!!']}").get(), + nullptr, &error); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("'abc'"), &error)); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("'def'"), &error)); + EXPECT_TRUE(ValidateValue(prop, *CreateValue("'xyz!!'"), &error)); + EXPECT_FALSE(ValidateValue(prop, *CreateValue("'xyz'"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); +} + +TEST(CommandSchema, StringPropType_CreateValue) { + StringPropType prop; + ErrorPtr error; + auto val = prop.CreateValue(base::StringValue{"blah"}, &error); + ASSERT_NE(nullptr, val.get()); + EXPECT_EQ(nullptr, error.get()); + EXPECT_EQ("blah", val->GetValue()); + + val = prop.CreateValue(base::FundamentalValue{4}, &error); + EXPECT_EQ(nullptr, val.get()); + ASSERT_NE(nullptr, error.get()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); +} + +/////////////////////////////////////////////////////////////////////////////// + +TEST(CommandSchema, ObjectPropType_Empty) { + ObjectPropType prop; + EXPECT_TRUE(prop.HasOverriddenAttributes()); + EXPECT_FALSE(prop.IsBasedOnSchema()); + EXPECT_EQ(nullptr, prop.GetDefaultValue()); + EXPECT_FALSE(prop.IsRequired()); +} + +TEST(CommandSchema, ObjectPropType_Types) { + ObjectPropType prop; + EXPECT_EQ(nullptr, prop.GetInt()); + EXPECT_EQ(nullptr, prop.GetBoolean()); + EXPECT_EQ(nullptr, prop.GetDouble()); + EXPECT_EQ(nullptr, prop.GetString()); + EXPECT_EQ(&prop, prop.GetObject()); + EXPECT_EQ(nullptr, prop.GetArray()); +} + +TEST(CommandSchema, ObjectPropType_ToJson) { + ObjectPropType prop; + EXPECT_JSON_EQ("{'additionalProperties':false,'properties':{}}", + *prop.ToJson(false, false)); + EXPECT_JSON_EQ( + "{'additionalProperties':false,'properties':{},'type':'object'}", + *prop.ToJson(true, false)); + EXPECT_FALSE(prop.IsBasedOnSchema()); + ObjectPropType prop2; + prop2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_JSON_EQ("{}", *prop2.ToJson(false, false)); + EXPECT_TRUE(prop2.IsBasedOnSchema()); + + auto schema = ObjectSchema::Create(); + schema->AddProp("expires", PropType::Create(ValueType::Int)); + auto pw = PropType::Create(ValueType::String); + pw->GetString()->AddLengthConstraint(6, 100); + schema->AddProp("password", std::move(pw)); + prop2.SetObjectSchema(std::move(schema)); + auto expected = R"({ + 'additionalProperties': false, + 'properties': { + 'expires': 'integer', + 'password': { + 'maxLength': 100, + 'minLength': 6 + } + } + })"; + EXPECT_JSON_EQ(expected, *prop2.ToJson(false, false)); + + expected = R"({ + 'additionalProperties': false, + 'properties': { + 'expires': { + 'type': 'integer' + }, + 'password': { + 'maxLength': 100, + 'minLength': 6, + 'type': 'string' + } + }, + 'type': 'object' + })"; + EXPECT_JSON_EQ(expected, *prop2.ToJson(true, false)); + + ObjectPropType prop3; + ASSERT_TRUE( + prop3.FromJson(CreateDictionaryValue( + "{'default':{'expires':3,'password':'abracadabra'}}") + .get(), + &prop2, nullptr)); + expected = R"({ + 'default': { + 'expires': 3, + 'password': 'abracadabra' + } + })"; + EXPECT_JSON_EQ(expected, *prop3.ToJson(false, false)); + + expected = R"({ + 'additionalProperties': false, + 'default': { + 'expires': 3, + 'password': 'abracadabra' + }, + 'properties': { + 'expires': { + 'type': 'integer' + }, + 'password': { + 'maxLength': 100, + 'minLength': 6, + 'type': 'string' + } + }, + 'type': 'object' + })"; + EXPECT_JSON_EQ(expected, *prop3.ToJson(true, false)); + + ObjectPropType prop4; + ASSERT_TRUE(prop4.FromJson( + CreateDictionaryValue("{'additionalProperties':true," + "'default':{'expires':3,'password':'abracadabra'}}") + .get(), + &prop2, nullptr)); + expected = R"({ + 'additionalProperties': true, + 'default': { + 'expires': 3, + 'password': 'abracadabra' + }, + 'properties': { + 'expires': 'integer', + 'password': { + 'maxLength': 100, + 'minLength': 6 + } + } + })"; + EXPECT_JSON_EQ(expected, *prop4.ToJson(false, false)); + + expected = R"({ + 'additionalProperties': true, + 'default': { + 'expires': 3, + 'password': 'abracadabra' + }, + 'properties': { + 'expires': { + 'type': 'integer' + }, + 'password': { + 'maxLength': 100, + 'minLength': 6, + 'type': 'string' + } + }, + 'type': 'object' + })"; + EXPECT_JSON_EQ(expected, *prop4.ToJson(true, false)); +} + +TEST(CommandSchema, ObjectPropType_FromJson) { + ObjectPropType base_prop; + EXPECT_TRUE(base_prop.FromJson( + CreateDictionaryValue("{'properties':{'name':'string','age':'integer'}}") + .get(), + nullptr, nullptr)); + auto schema = base_prop.GetObject()->GetObjectSchemaPtr(); + const PropType* prop = schema->GetProp("name"); + EXPECT_EQ(ValueType::String, prop->GetType()); + prop = schema->GetProp("age"); + EXPECT_EQ(ValueType::Int, prop->GetType()); + + ObjectPropType prop2; + ASSERT_TRUE(prop2.FromJson( + CreateDictionaryValue("{'properties':{'name':'string','age':'integer'}," + "'default':{'name':'Bob','age':33}}") + .get(), + nullptr, nullptr)); + ASSERT_NE(nullptr, prop2.GetDefaultValue()); + const ObjectValue* defval = prop2.GetDefaultValue()->GetObject(); + ASSERT_NE(nullptr, defval); + ValueMap objval = defval->GetValue(); + EXPECT_EQ("Bob", objval["name"]->GetString()->GetValue()); + EXPECT_EQ(33, objval["age"]->GetInt()->GetValue()); +} + +TEST(CommandSchema, ObjectPropType_Validate) { + ObjectPropType prop; + prop.FromJson( + CreateDictionaryValue("{'properties':{'expires':'integer'," + "'password':{'maxLength':100,'minLength':6}}," + "'required':['expires','password']}") + .get(), + nullptr, nullptr); + ErrorPtr error; + EXPECT_TRUE(ValidateValue( + prop, *CreateValue("{'expires':10,'password':'abcdef'}"), &error)); + error.reset(); + + EXPECT_FALSE(ValidateValue(prop, *CreateValue("{'expires':10}"), &error)); + EXPECT_EQ("parameter_missing", error->GetCode()); + error.reset(); + + EXPECT_FALSE( + ValidateValue(prop, *CreateValue("{'password':'abcdef'}"), &error)); + EXPECT_EQ("parameter_missing", error->GetCode()); + error.reset(); + + EXPECT_FALSE(ValidateValue( + prop, *CreateValue("{'expires':10,'password':'abcde'}"), &error)); + EXPECT_EQ("out_of_range", error->GetFirstError()->GetCode()); + error.reset(); + + EXPECT_FALSE(ValidateValue(prop, *CreateValue("2"), &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); + error.reset(); + + EXPECT_FALSE(ValidateValue( + prop, *CreateValue("{'expires':10,'password':'abcdef','retry':true}"), + &error)); + EXPECT_EQ("unexpected_parameter", error->GetCode()); + error.reset(); +} + +TEST(CommandSchema, ObjectPropType_Validate_Enum) { + ObjectPropType prop; + EXPECT_TRUE(prop.FromJson( + CreateDictionaryValue( + "{'properties':{'width':'integer','height':'integer'}," + "'enum':[{'width':10,'height':20},{'width':100,'height':200}]}") + .get(), + nullptr, nullptr)); + ErrorPtr error; + EXPECT_TRUE( + ValidateValue(prop, *CreateValue("{'height':20,'width':10}"), &error)); + error.reset(); + + EXPECT_TRUE( + ValidateValue(prop, *CreateValue("{'height':200,'width':100}"), &error)); + error.reset(); + + EXPECT_FALSE( + ValidateValue(prop, *CreateValue("{'height':12,'width':10}"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); +} + +TEST(CommandSchema, ObjectPropType_CreateValue) { + ObjectPropType prop; + IntPropType int_type; + ASSERT_TRUE(prop.FromJson( + CreateDictionaryValue( + "{'properties':{'width':'integer','height':'integer'}," + "'enum':[{'width':10,'height':20},{'width':100,'height':200}]}") + .get(), + nullptr, nullptr)); + ValueMap obj{ + {"width", int_type.CreateValue(base::FundamentalValue{10}, nullptr)}, + {"height", int_type.CreateValue(base::FundamentalValue{20}, nullptr)}, + }; + + ErrorPtr error; + auto val = prop.CreateValue( + *CreateDictionaryValue("{'width': 10, 'height': 20}"), &error); + ASSERT_NE(nullptr, val.get()); + EXPECT_EQ(nullptr, error.get()); + EXPECT_EQ(obj, val->GetValue()); + + val = prop.CreateValue(base::StringValue{"blah"}, &error); + EXPECT_EQ(nullptr, val.get()); + ASSERT_NE(nullptr, error.get()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); +} + +/////////////////////////////////////////////////////////////////////////////// + +TEST(CommandSchema, ArrayPropType_Empty) { + ArrayPropType prop; + EXPECT_FALSE(prop.HasOverriddenAttributes()); + EXPECT_FALSE(prop.IsBasedOnSchema()); + EXPECT_EQ(nullptr, prop.GetDefaultValue()); + EXPECT_EQ(nullptr, prop.GetItemTypePtr()); + prop.SetItemType(PropType::Create(ValueType::Int)); + EXPECT_TRUE(prop.HasOverriddenAttributes()); + EXPECT_FALSE(prop.IsBasedOnSchema()); + EXPECT_NE(nullptr, prop.GetItemTypePtr()); + EXPECT_FALSE(prop.IsRequired()); +} + +TEST(CommandSchema, ArrayPropType_Types) { + 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) { + ArrayPropType prop; + prop.SetItemType(PropType::Create(ValueType::Int)); + EXPECT_JSON_EQ("{'items':'integer'}", *prop.ToJson(false, false)); + EXPECT_JSON_EQ("{'items':{'type':'integer'},'type':'array'}", + *prop.ToJson(true, false)); + EXPECT_FALSE(prop.IsBasedOnSchema()); + ArrayPropType prop2; + prop2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr); + EXPECT_JSON_EQ("{}", *prop2.ToJson(false, false)); + EXPECT_TRUE(prop2.IsBasedOnSchema()); + prop2.FromJson(CreateDictionaryValue("{'default':[1,2,3]}").get(), &prop, + nullptr); + EXPECT_JSON_EQ("{'default':[1,2,3]}", *prop2.ToJson(false, false)); + EXPECT_JSON_EQ( + "{'default':[1,2,3],'items':{'type':'integer'},'type':'array'}", + *prop2.ToJson(true, false)); +} + +TEST(CommandSchema, ArrayPropType_FromJson) { + ArrayPropType prop; + EXPECT_TRUE(prop.FromJson(CreateDictionaryValue("{'items':'integer'}").get(), + nullptr, nullptr)); + EXPECT_EQ(ValueType::Int, prop.GetItemTypePtr()->GetType()); + + ArrayPropType prop2; + ASSERT_TRUE( + prop2.FromJson(CreateDictionaryValue( + "{'items':'string','default':['foo', 'bar', 'baz']}") + .get(), + nullptr, nullptr)); + ASSERT_NE(nullptr, prop2.GetDefaultValue()); + const 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) { + ArrayPropType prop; + prop.FromJson( + CreateDictionaryValue("{'items':{'minimum':2.3, 'maximum':10.5}}").get(), + nullptr, nullptr); + + ErrorPtr error; + EXPECT_TRUE(ValidateValue(prop, *CreateValue("[3,4,10.5]"), &error)); + error.reset(); + + EXPECT_FALSE(ValidateValue(prop, *CreateValue("[2]"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + + EXPECT_FALSE(ValidateValue(prop, *CreateValue("[4, 5, 20]"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); +} + +TEST(CommandSchema, ArrayPropType_Validate_Enum) { + ArrayPropType prop; + prop.FromJson( + CreateDictionaryValue("{'items':'integer', 'enum':[[1], [2,3], [4,5,6]]}") + .get(), + nullptr, nullptr); + + ErrorPtr error; + EXPECT_TRUE(ValidateValue(prop, *CreateValue("[2,3]"), &error)); + error.reset(); + + EXPECT_FALSE(ValidateValue(prop, *CreateValue("[2]"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); + + EXPECT_FALSE(ValidateValue(prop, *CreateValue("[2,3,4]"), &error)); + EXPECT_EQ("out_of_range", error->GetCode()); + error.reset(); +} + +TEST(CommandSchema, ArrayPropType_CreateValue) { + ArrayPropType prop; + ASSERT_TRUE(prop.FromJson( + CreateDictionaryValue( + "{'items':{'properties':{'width':'integer','height':'integer'}}}") + .get(), + nullptr, nullptr)); + + ErrorPtr error; + ValueVector arr; + + auto val = prop.CreateValue(base::ListValue{}, &error); + ASSERT_NE(nullptr, val.get()); + EXPECT_EQ(nullptr, error.get()); + EXPECT_EQ(arr, val->GetValue()); + EXPECT_JSON_EQ("[]", *val->ToJson()); + + val = prop.CreateValue( + *CreateValue("[{'height':20,'width':10},{'width':17, 'height':18}]"), + &error); + ASSERT_NE(nullptr, val.get()); + EXPECT_EQ(nullptr, error.get()); + EXPECT_JSON_EQ("[{'height':20,'width':10},{'height':18,'width':17}]", + *val->ToJson()); + + val = prop.CreateValue(base::StringValue{"blah"}, &error); + EXPECT_EQ(nullptr, val.get()); + ASSERT_NE(nullptr, error.get()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); +} + +TEST(CommandSchema, ArrayPropType_NestedArrays_NotSupported) { + ArrayPropType prop; + ErrorPtr error; + EXPECT_FALSE(prop.FromJson( + CreateDictionaryValue("{'items':{'items':'integer'}}").get(), nullptr, + &error)); + EXPECT_EQ(errors::commands::kInvalidObjectSchema, error->GetCode()); + error.reset(); +} + +/////////////////////////////////////////////////////////////////////////////// + +TEST(CommandSchema, ObjectSchema_FromJson_Shorthand_TypeName) { + ObjectSchema schema; + const char* schema_str = + "{" + "'param1':'integer'," + "'param2':'number'," + "'param3':'string'" + "}"; + EXPECT_TRUE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + nullptr)); + EXPECT_EQ(ValueType::Int, schema.GetProp("param1")->GetType()); + EXPECT_EQ(ValueType::Double, schema.GetProp("param2")->GetType()); + EXPECT_EQ(ValueType::String, schema.GetProp("param3")->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")); + + int min_int = (std::numeric_limits<int>::min)(); + int max_int = (std::numeric_limits<int>::max)(); + double min_dbl = (std::numeric_limits<double>::lowest)(); + double max_dbl = (std::numeric_limits<double>::max)(); + EXPECT_EQ(min_int, schema.GetProp("param1")->GetInt()->GetMinValue()); + EXPECT_EQ(max_int, schema.GetProp("param1")->GetInt()->GetMaxValue()); + EXPECT_EQ(min_dbl, schema.GetProp("param2")->GetDouble()->GetMinValue()); + EXPECT_EQ(max_dbl, schema.GetProp("param2")->GetDouble()->GetMaxValue()); + EXPECT_EQ(0, schema.GetProp("param3")->GetString()->GetMinLength()); + EXPECT_EQ(max_int, schema.GetProp("param3")->GetString()->GetMaxLength()); +} + +TEST(CommandSchema, ObjectSchema_FromJson_Full_TypeName) { + ObjectSchema schema; + const char* schema_str = + "{" + "'param1':{'type':'integer'}," + "'param2':{'type':'number'}," + "'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(ValueType::Int, schema.GetProp("param1")->GetType()); + EXPECT_EQ(ValueType::Double, schema.GetProp("param2")->GetType()); + EXPECT_EQ(ValueType::String, schema.GetProp("param3")->GetType()); + EXPECT_EQ(ValueType::Array, schema.GetProp("param4")->GetType()); + EXPECT_EQ(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("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)(); + double min_dbl = (std::numeric_limits<double>::lowest)(); + double max_dbl = (std::numeric_limits<double>::max)(); + EXPECT_EQ(min_int, schema.GetProp("param1")->GetInt()->GetMinValue()); + EXPECT_EQ(max_int, schema.GetProp("param1")->GetInt()->GetMaxValue()); + EXPECT_EQ(min_dbl, schema.GetProp("param2")->GetDouble()->GetMinValue()); + EXPECT_EQ(max_dbl, schema.GetProp("param2")->GetDouble()->GetMaxValue()); + EXPECT_EQ(0, schema.GetProp("param3")->GetString()->GetMinLength()); + EXPECT_EQ(max_int, schema.GetProp("param3")->GetString()->GetMaxLength()); +} + +TEST(CommandSchema, ObjectSchema_FromJson_Shorthand_TypeDeduction_Scalar) { + ObjectSchema schema; + const char* schema_str = + "{" + "'param1' :{'minimum':2}," + "'param2' :{'maximum':10}," + "'param3' :{'maximum':8, 'minimum':2}," + "'param4' :{'minimum':2.1}," + "'param5' :{'maximum':10.1}," + "'param6' :{'maximum':8.1, 'minimum':3.1}," + "'param7' :{'maximum':8, 'minimum':3.1}," + "'param8' :{'maximum':8.1, 'minimum':3}," + "'param9' :{'minLength':2}," + "'param10':{'maxLength':10}," + "'param11':{'maxLength':8, 'minLength':3}," + "'param12':{'default':12}," + "'param13':{'default':13.5}," + "'param14':{'default':true}," + "'param15':{'default':false}," + "'param16':{'default':'foobar'}," + "'param17':{'default':[1,2,3]}," + "'param18':{'items':'number', 'default':[]}" + "}"; + EXPECT_TRUE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + nullptr)); + EXPECT_EQ("integer", schema.GetProp("param1")->GetTypeAsString()); + EXPECT_EQ("integer", schema.GetProp("param2")->GetTypeAsString()); + EXPECT_EQ("integer", schema.GetProp("param3")->GetTypeAsString()); + EXPECT_EQ("number", schema.GetProp("param4")->GetTypeAsString()); + EXPECT_EQ("number", schema.GetProp("param5")->GetTypeAsString()); + EXPECT_EQ("number", schema.GetProp("param6")->GetTypeAsString()); + EXPECT_EQ("number", schema.GetProp("param7")->GetTypeAsString()); + EXPECT_EQ("number", schema.GetProp("param8")->GetTypeAsString()); + EXPECT_EQ("string", schema.GetProp("param9")->GetTypeAsString()); + EXPECT_EQ("string", schema.GetProp("param10")->GetTypeAsString()); + EXPECT_EQ("string", schema.GetProp("param11")->GetTypeAsString()); + EXPECT_EQ("integer", schema.GetProp("param12")->GetTypeAsString()); + EXPECT_EQ("number", schema.GetProp("param13")->GetTypeAsString()); + 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)(); + double min_dbl = (std::numeric_limits<double>::lowest)(); + double max_dbl = (std::numeric_limits<double>::max)(); + EXPECT_EQ(2, schema.GetProp("param1")->GetInt()->GetMinValue()); + EXPECT_EQ(max_int, schema.GetProp("param1")->GetInt()->GetMaxValue()); + EXPECT_EQ(min_int, schema.GetProp("param2")->GetInt()->GetMinValue()); + EXPECT_EQ(10, schema.GetProp("param2")->GetInt()->GetMaxValue()); + EXPECT_EQ(2, schema.GetProp("param3")->GetInt()->GetMinValue()); + EXPECT_EQ(8, schema.GetProp("param3")->GetInt()->GetMaxValue()); + EXPECT_DOUBLE_EQ(2.1, schema.GetProp("param4")->GetDouble()->GetMinValue()); + EXPECT_DOUBLE_EQ(max_dbl, + schema.GetProp("param4")->GetDouble()->GetMaxValue()); + EXPECT_DOUBLE_EQ(min_dbl, + schema.GetProp("param5")->GetDouble()->GetMinValue()); + EXPECT_DOUBLE_EQ(10.1, schema.GetProp("param5")->GetDouble()->GetMaxValue()); + EXPECT_DOUBLE_EQ(3.1, schema.GetProp("param6")->GetDouble()->GetMinValue()); + EXPECT_DOUBLE_EQ(8.1, schema.GetProp("param6")->GetDouble()->GetMaxValue()); + EXPECT_DOUBLE_EQ(3.1, schema.GetProp("param7")->GetDouble()->GetMinValue()); + EXPECT_DOUBLE_EQ(8.0, schema.GetProp("param7")->GetDouble()->GetMaxValue()); + EXPECT_DOUBLE_EQ(3.0, schema.GetProp("param8")->GetDouble()->GetMinValue()); + EXPECT_DOUBLE_EQ(8.1, schema.GetProp("param8")->GetDouble()->GetMaxValue()); + EXPECT_EQ(2, schema.GetProp("param9")->GetString()->GetMinLength()); + EXPECT_EQ(max_int, schema.GetProp("param9")->GetString()->GetMaxLength()); + EXPECT_EQ(0, schema.GetProp("param10")->GetString()->GetMinLength()); + EXPECT_EQ(10, schema.GetProp("param10")->GetString()->GetMaxLength()); + EXPECT_EQ(3, schema.GetProp("param11")->GetString()->GetMinLength()); + EXPECT_EQ(8, schema.GetProp("param11")->GetString()->GetMaxLength()); + const PropValue* val = schema.GetProp("param12")->GetDefaultValue(); + EXPECT_EQ(12, val->GetInt()->GetValue()); + val = schema.GetProp("param13")->GetDefaultValue(); + EXPECT_DOUBLE_EQ(13.5, val->GetDouble()->GetValue()); + val = schema.GetProp("param14")->GetDefaultValue(); + EXPECT_TRUE(val->GetBoolean()->GetValue()); + val = schema.GetProp("param15")->GetDefaultValue(); + 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) { + ObjectSchema schema; + const char* schema_str = + "{" + "'param1' :[0,1,2,3]," + "'param2' :[0.0,1.1,2.2]," + "'param3' :['id1', 'id2']," + "'param4' :{'enum':[1,2,3]}," + "'param5' :{'enum':[-1.1,2.2,3]}," + "'param6' :{'enum':['id0', 'id1']}," + "'param7' :{'type':'integer', 'enum':[1,2,3]}," + "'param8' :{'type':'number', 'enum':[1,2,3]}," + "'param9' :{'type':'number', '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()); + EXPECT_EQ("number", schema.GetProp("param2")->GetTypeAsString()); + EXPECT_EQ("string", schema.GetProp("param3")->GetTypeAsString()); + EXPECT_EQ("integer", schema.GetProp("param4")->GetTypeAsString()); + EXPECT_EQ("number", schema.GetProp("param5")->GetTypeAsString()); + EXPECT_EQ("string", schema.GetProp("param6")->GetTypeAsString()); + EXPECT_EQ("integer", schema.GetProp("param7")->GetTypeAsString()); + EXPECT_EQ("number", schema.GetProp("param8")->GetTypeAsString()); + 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}), + GetOneOfValues<double>(schema.GetProp("param2"))); + EXPECT_EQ((std::vector<std::string>{"id1", "id2"}), + GetOneOfValues<std::string>(schema.GetProp("param3"))); + + EXPECT_EQ((std::vector<int>{1, 2, 3}), + GetOneOfValues<int>(schema.GetProp("param4"))); + EXPECT_EQ((std::vector<double>{-1.1, 2.2, 3.0}), + GetOneOfValues<double>(schema.GetProp("param5"))); + EXPECT_EQ((std::vector<std::string>{"id0", "id1"}), + GetOneOfValues<std::string>(schema.GetProp("param6"))); + EXPECT_EQ((std::vector<int>{1, 2, 3}), + GetOneOfValues<int>(schema.GetProp("param7"))); + EXPECT_EQ((std::vector<double>{1.0, 2.0, 3.0}), + GetOneOfValues<double>(schema.GetProp("param8"))); + EXPECT_TRUE(GetOneOfValues<double>(schema.GetProp("param9")).empty()); + EXPECT_TRUE(GetOneOfValues<int>(schema.GetProp("param10")).empty()); +} + +TEST(CommandSchema, ObjectSchema_FromJson_Inheritance) { + const char* base_schema_str = + "{" + "'param0' :{'minimum':1, 'maximum':5}," + "'param1' :{'minimum':1, 'maximum':5}," + "'param2' :{'minimum':1, 'maximum':5}," + "'param3' :{'minimum':1, 'maximum':5}," + "'param4' :{'minimum':1, 'maximum':5}," + "'param5' :{'minimum':1.1, 'maximum':5.5}," + "'param6' :{'minimum':1.1, 'maximum':5.5}," + "'param7' :{'minimum':1.1, 'maximum':5.5}," + "'param8' :{'minimum':1.1, 'maximum':5.5}," + "'param9' :{'minLength':1, 'maxLength':5}," + "'param10':{'minLength':1, 'maxLength':5}," + "'param11':{'minLength':1, 'maxLength':5}," + "'param12':{'minLength':1, 'maxLength':5}," + "'param13':[1,2,3]," + "'param14':[1,2,3]," + "'param15':[1.1,2.2,3.3]," + "'param16':[1.1,2.2,3.3]," + "'param17':['id1', 'id2']," + "'param18':['id1', 'id2']," + "'param19':{'minimum':1, 'maximum':5}," + "'param20':{'default':49}," + "'param21':{'default':49}," + "'param22':'integer'" + "}"; + ObjectSchema base_schema; + EXPECT_TRUE(base_schema.FromJson(CreateDictionaryValue(base_schema_str).get(), + nullptr, nullptr)); + const char* schema_str = + "{" + "'param1' :{}," + "'param2' :{'minimum':2}," + "'param3' :{'maximum':9}," + "'param4' :{'minimum':2, 'maximum':9}," + "'param5' :{}," + "'param6' :{'minimum':2.2}," + "'param7' :{'maximum':9.9}," + "'param8' :{'minimum':2.2, 'maximum':9.9}," + "'param9' :{}," + "'param10':{'minLength':3}," + "'param11':{'maxLength':8}," + "'param12':{'minLength':3, 'maxLength':8}," + "'param13':{}," + "'param14':[1,2,3,4]," + "'param15':{}," + "'param16':[1.1,2.2,3.3,4.4]," + "'param17':{}," + "'param18':['id1', 'id3']," + "'param19':{}," + "'param20':{}," + "'param21':{'default':8}," + "'param22':{'default':123}" + "}"; + ObjectSchema schema; + EXPECT_TRUE(schema.FromJson(CreateDictionaryValue(schema_str).get(), + &base_schema, nullptr)); + EXPECT_EQ(nullptr, schema.GetProp("param0")); + EXPECT_NE(nullptr, schema.GetProp("param1")); + EXPECT_EQ("integer", schema.GetProp("param1")->GetTypeAsString()); + EXPECT_EQ(1, schema.GetProp("param1")->GetInt()->GetMinValue()); + EXPECT_EQ(5, schema.GetProp("param1")->GetInt()->GetMaxValue()); + EXPECT_EQ("integer", schema.GetProp("param2")->GetTypeAsString()); + EXPECT_EQ(2, schema.GetProp("param2")->GetInt()->GetMinValue()); + EXPECT_EQ(5, schema.GetProp("param2")->GetInt()->GetMaxValue()); + EXPECT_EQ("integer", schema.GetProp("param3")->GetTypeAsString()); + EXPECT_EQ(1, schema.GetProp("param3")->GetInt()->GetMinValue()); + EXPECT_EQ(9, schema.GetProp("param3")->GetInt()->GetMaxValue()); + EXPECT_EQ("integer", schema.GetProp("param4")->GetTypeAsString()); + EXPECT_EQ(2, schema.GetProp("param4")->GetInt()->GetMinValue()); + EXPECT_EQ(9, schema.GetProp("param4")->GetInt()->GetMaxValue()); + EXPECT_EQ("number", schema.GetProp("param5")->GetTypeAsString()); + EXPECT_EQ(1.1, schema.GetProp("param5")->GetDouble()->GetMinValue()); + EXPECT_EQ(5.5, schema.GetProp("param5")->GetDouble()->GetMaxValue()); + EXPECT_EQ("number", schema.GetProp("param6")->GetTypeAsString()); + EXPECT_EQ(2.2, schema.GetProp("param6")->GetDouble()->GetMinValue()); + EXPECT_EQ(5.5, schema.GetProp("param6")->GetDouble()->GetMaxValue()); + EXPECT_EQ("number", schema.GetProp("param7")->GetTypeAsString()); + EXPECT_EQ(1.1, schema.GetProp("param7")->GetDouble()->GetMinValue()); + EXPECT_EQ(9.9, schema.GetProp("param7")->GetDouble()->GetMaxValue()); + EXPECT_EQ("number", schema.GetProp("param8")->GetTypeAsString()); + EXPECT_EQ(2.2, schema.GetProp("param8")->GetDouble()->GetMinValue()); + EXPECT_EQ(9.9, schema.GetProp("param8")->GetDouble()->GetMaxValue()); + EXPECT_EQ("string", schema.GetProp("param9")->GetTypeAsString()); + EXPECT_EQ(1, schema.GetProp("param9")->GetString()->GetMinLength()); + EXPECT_EQ(5, schema.GetProp("param9")->GetString()->GetMaxLength()); + EXPECT_EQ("string", schema.GetProp("param10")->GetTypeAsString()); + EXPECT_EQ(3, schema.GetProp("param10")->GetString()->GetMinLength()); + EXPECT_EQ(5, schema.GetProp("param10")->GetString()->GetMaxLength()); + EXPECT_EQ("string", schema.GetProp("param11")->GetTypeAsString()); + EXPECT_EQ(1, schema.GetProp("param11")->GetString()->GetMinLength()); + EXPECT_EQ(8, schema.GetProp("param11")->GetString()->GetMaxLength()); + EXPECT_EQ("string", schema.GetProp("param12")->GetTypeAsString()); + EXPECT_EQ(3, schema.GetProp("param12")->GetString()->GetMinLength()); + EXPECT_EQ(8, schema.GetProp("param12")->GetString()->GetMaxLength()); + EXPECT_EQ("integer", schema.GetProp("param13")->GetTypeAsString()); + EXPECT_EQ((std::vector<int>{1, 2, 3}), + GetOneOfValues<int>(schema.GetProp("param13"))); + EXPECT_EQ("integer", schema.GetProp("param14")->GetTypeAsString()); + EXPECT_EQ((std::vector<int>{1, 2, 3, 4}), + GetOneOfValues<int>(schema.GetProp("param14"))); + EXPECT_EQ("number", schema.GetProp("param15")->GetTypeAsString()); + EXPECT_EQ((std::vector<double>{1.1, 2.2, 3.3}), + GetOneOfValues<double>(schema.GetProp("param15"))); + EXPECT_EQ("number", schema.GetProp("param16")->GetTypeAsString()); + EXPECT_EQ((std::vector<double>{1.1, 2.2, 3.3, 4.4}), + GetOneOfValues<double>(schema.GetProp("param16"))); + EXPECT_EQ("string", schema.GetProp("param17")->GetTypeAsString()); + EXPECT_EQ((std::vector<std::string>{"id1", "id2"}), + GetOneOfValues<std::string>(schema.GetProp("param17"))); + EXPECT_EQ("string", schema.GetProp("param18")->GetTypeAsString()); + EXPECT_EQ((std::vector<std::string>{"id1", "id3"}), + GetOneOfValues<std::string>(schema.GetProp("param18"))); + EXPECT_EQ("integer", schema.GetProp("param19")->GetTypeAsString()); + EXPECT_EQ(1, schema.GetProp("param19")->GetInt()->GetMinValue()); + EXPECT_EQ(5, schema.GetProp("param19")->GetInt()->GetMaxValue()); + EXPECT_EQ(49, + schema.GetProp("param20")->GetDefaultValue()->GetInt()->GetValue()); + EXPECT_EQ(8, + schema.GetProp("param21")->GetDefaultValue()->GetInt()->GetValue()); + EXPECT_EQ(123, + schema.GetProp("param22")->GetDefaultValue()->GetInt()->GetValue()); +} + +TEST(CommandSchema, ObjectSchema_UseDefaults) { + ObjectPropType prop; + const char* schema_str = + "{'properties':{" + "'param1':{'default':true}," + "'param2':{'default':2}," + "'param3':{'default':3.3}," + "'param4':{'default':'four'}," + "'param5':{'default':{'x':5,'y':6}," + "'properties':{'x':'integer','y':'integer'}}," + "'param6':{'default':[1,2,3]}" + "}}"; + ASSERT_TRUE( + prop.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, nullptr)); + + // Omit all. + auto value = + prop.CreatePropValue(*CreateDictionaryValue("{}").get(), nullptr); + ASSERT_NE(nullptr, value); + ValueMap obj = value->GetObject()->GetValue(); + EXPECT_TRUE(obj["param1"]->GetBoolean()->GetValue()); + EXPECT_EQ(2, obj["param2"]->GetInt()->GetValue()); + EXPECT_DOUBLE_EQ(3.3, obj["param3"]->GetDouble()->GetValue()); + EXPECT_EQ("four", obj["param4"]->GetString()->GetValue()); + ValueMap param5 = obj["param5"]->GetObject()->GetValue(); + EXPECT_EQ(5, param5["x"]->GetInt()->GetValue()); + EXPECT_EQ(6, param5["y"]->GetInt()->GetValue()); + ValueVector param6 = obj["param6"]->GetArray()->GetValue(); + EXPECT_EQ((std::vector<int>{1, 2, 3}), GetArrayValues<int>(param6)); + + // Specify some. + const char* val_json = + "{" + "'param1':false," + "'param3':33.3," + "'param5':{'x':-5,'y':-6}" + "}"; + value = prop.CreatePropValue(*CreateDictionaryValue(val_json).get(), nullptr); + ASSERT_NE(nullptr, value); + obj = value->GetObject()->GetValue(); + EXPECT_FALSE(obj["param1"]->GetBoolean()->GetValue()); + EXPECT_EQ(2, obj["param2"]->GetInt()->GetValue()); + EXPECT_DOUBLE_EQ(33.3, obj["param3"]->GetDouble()->GetValue()); + EXPECT_EQ("four", obj["param4"]->GetString()->GetValue()); + 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. + val_json = + "{" + "'param1':false," + "'param2':22," + "'param3':333.3," + "'param4':'FOUR'," + "'param5':{'x':-55,'y':66}," + "'param6':[-1, 0]" + "}"; + value = prop.CreatePropValue(*CreateDictionaryValue(val_json).get(), nullptr); + ASSERT_NE(nullptr, value); + obj = value->GetObject()->GetValue(); + EXPECT_FALSE(obj["param1"]->GetBoolean()->GetValue()); + EXPECT_EQ(22, obj["param2"]->GetInt()->GetValue()); + EXPECT_DOUBLE_EQ(333.3, obj["param3"]->GetDouble()->GetValue()); + EXPECT_EQ("FOUR", obj["param4"]->GetString()->GetValue()); + 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) { + ObjectSchema schema; + ErrorPtr error; + const char* schema_str = + "{" + "'param1':{}" + "}"; + EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + &error)); + EXPECT_EQ("no_type_info", error->GetFirstError()->GetCode()); + error.reset(); + + schema_str = + "{" + "'param1':{'type':'foo'}" + "}"; + EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + &error)); + EXPECT_EQ("unknown_type", error->GetFirstError()->GetCode()); + error.reset(); + + schema_str = + "{" + "'param1':[]" + "}"; + EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + &error)); + EXPECT_EQ("no_type_info", error->GetFirstError()->GetCode()); + error.reset(); + + schema_str = + "{" + "'param1':{'minimum':'foo'}" + "}"; + EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + &error)); + EXPECT_EQ("type_mismatch", error->GetFirstError()->GetCode()); + error.reset(); + + schema_str = + "{" + "'param1':[1,2.2]" + "}"; + EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + &error)); + EXPECT_EQ("type_mismatch", error->GetFirstError()->GetCode()); + error.reset(); + + schema_str = + "{" + "'param1':{'minimum':1, 'enum':[1,2,3]}" // can't have min/max & enum. + "}"; + EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + &error)); + EXPECT_EQ("unexpected_parameter", error->GetFirstError()->GetCode()); + error.reset(); + + schema_str = + "{" + "'param1':{'maximum':1, 'blah':2}" // 'blah' is unexpected. + "}"; + EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + &error)); + EXPECT_EQ("unexpected_parameter", error->GetFirstError()->GetCode()); + error.reset(); + + schema_str = + "{" + "'param1':{'enum':[1,2,3],'default':5}" // 'default' must be 1, 2, or 3. + "}"; + EXPECT_FALSE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + &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(); +} + +TEST(CommandSchema, RequiredProperties_Integral) { + IntPropType prop; + + prop.MakeRequired(false); + EXPECT_JSON_EQ("{'type':'integer'}", *prop.ToJson(true, false)); + EXPECT_JSON_EQ("{'isRequired':false,'type':'integer'}", + *prop.ToJson(true, true)); + + prop.MakeRequired(true); + EXPECT_JSON_EQ("{'type':'integer'}", *prop.ToJson(true, false)); + EXPECT_JSON_EQ("{'isRequired':true,'type':'integer'}", + *prop.ToJson(true, true)); + + IntPropType prop2; + EXPECT_TRUE( + prop2.FromJson(CreateDictionaryValue("{}").get(), &prop, nullptr)); + EXPECT_TRUE(prop2.IsRequired()); + + EXPECT_TRUE(prop2.FromJson( + CreateDictionaryValue("{'isRequired': false}").get(), &prop, nullptr)); + EXPECT_FALSE(prop2.IsRequired()); + + EXPECT_JSON_EQ("{'type':'integer'}", *prop2.ToJson(true, false)); + EXPECT_JSON_EQ("{'isRequired':false,'type':'integer'}", + *prop2.ToJson(true, true)); +} + +TEST(CommandSchema, RequiredProperties_Object) { + ObjectPropType obj_type; + auto schema = ObjectSchema::Create(); + auto type = PropType::Create(ValueType::Int); + type->MakeRequired(true); + schema->AddProp("prop1", std::move(type)); + type = PropType::Create(ValueType::String); + type->MakeRequired(false); + schema->AddProp("prop2", std::move(type)); + type = PropType::Create(ValueType::Boolean); + type->MakeRequired(true); + schema->AddProp("prop3", std::move(type)); + type = PropType::Create(ValueType::Array); + type->GetArray()->SetItemType(PropType::Create(ValueType::String)); + schema->AddProp("prop4", std::move(type)); + auto expected1 = R"({ + 'prop1': 'integer', + 'prop2': 'string', + 'prop3': 'boolean', + 'prop4': {'items': 'string'} + })"; + EXPECT_JSON_EQ(expected1, *schema->ToJson(false, false)); + auto expected2 = R"({ + 'prop1': {'type':'integer','isRequired': true}, + 'prop2': {'type':'string','isRequired': false}, + 'prop3': {'type':'boolean','isRequired': true}, + 'prop4': {'items': 'string'} + })"; + EXPECT_JSON_EQ(expected2, *schema->ToJson(false, true)); + + obj_type.SetObjectSchema(std::move(schema)); + auto expected3 = R"({ + 'additionalProperties': false, + 'properties': { + 'prop1': 'integer', + 'prop2': 'string', + 'prop3': 'boolean', + 'prop4': {'items': 'string'} + }, + 'required': ['prop1','prop3'] + })"; + EXPECT_JSON_EQ(expected3, *obj_type.ToJson(false, false)); + EXPECT_JSON_EQ(expected3, *obj_type.ToJson(false, true)); +} + +TEST(CommandSchema, RequiredProperties_Schema_FromJson) { + ObjectSchema schema; + auto schema_str = R"({ + 'prop1': {'type':'integer','isRequired': true}, + 'prop2': {'type':'string','isRequired': false}, + 'prop3': 'boolean' + })"; + EXPECT_TRUE(schema.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, + nullptr)); + EXPECT_TRUE(schema.GetProp("prop1")->IsRequired()); + EXPECT_FALSE(schema.GetProp("prop2")->IsRequired()); + EXPECT_FALSE(schema.GetProp("prop3")->IsRequired()); + EXPECT_JSON_EQ(schema_str, *schema.ToJson(false, true)); +} + +TEST(CommandSchema, RequiredProperties_Schema_FromJson_Inherit) { + ObjectSchema base_schema; + auto base_schema_str = R"({ + 'prop1': {'type':'integer','isRequired': true}, + 'prop2': {'type':'integer','isRequired': false}, + 'prop3': {'type':'integer','isRequired': true}, + 'prop4': {'type':'integer','isRequired': false} + })"; + EXPECT_TRUE(base_schema.FromJson(CreateDictionaryValue(base_schema_str).get(), + nullptr, nullptr)); + ObjectSchema schema; + auto schema_str = R"({ + 'prop1': {'isRequired': false}, + 'prop2': {'isRequired': true}, + 'prop3': {}, + 'prop4': 'integer' + })"; + EXPECT_TRUE(schema.FromJson(CreateDictionaryValue(schema_str).get(), + &base_schema, nullptr)); + EXPECT_FALSE(schema.GetProp("prop1")->IsRequired()); + EXPECT_TRUE(schema.GetProp("prop2")->IsRequired()); + EXPECT_TRUE(schema.GetProp("prop3")->IsRequired()); + EXPECT_FALSE(schema.GetProp("prop4")->IsRequired()); + auto expected = R"({ + 'prop1': {'type':'integer','isRequired': false}, + 'prop2': {'type':'integer','isRequired': true}, + 'prop3': {}, + 'prop4': {} + })"; + EXPECT_JSON_EQ(expected, *schema.ToJson(false, true)); +} + +TEST(CommandSchema, RequiredProperties_ObjectPropType_FromJson) { + ObjectPropType obj_type; + auto type_str = R"({ + 'properties': { + 'prop1': 'integer', + 'prop2': 'string', + 'prop3': {'type':'boolean','isRequired':true}, + 'prop4': {'items': 'string','isRequired':false}, + 'prop5': {'type':'number','isRequired':true} + }, + 'required': ['prop1','prop3','prop4','prop5'] + })"; + EXPECT_TRUE(obj_type.FromJson(CreateDictionaryValue(type_str).get(), nullptr, + nullptr)); + EXPECT_TRUE(obj_type.GetObjectSchemaPtr()->GetProp("prop1")->IsRequired()); + EXPECT_FALSE(obj_type.GetObjectSchemaPtr()->GetProp("prop2")->IsRequired()); + EXPECT_TRUE(obj_type.GetObjectSchemaPtr()->GetProp("prop3")->IsRequired()); + // 'required' takes precedence over 'isRequired'. + EXPECT_TRUE(obj_type.GetObjectSchemaPtr()->GetProp("prop4")->IsRequired()); + EXPECT_TRUE(obj_type.GetObjectSchemaPtr()->GetProp("prop5")->IsRequired()); +} + +TEST(CommandSchema, RequiredProperties_Failures) { + ObjectPropType obj_type; + ErrorPtr error; + + auto type_str = R"({ + 'properties': { + 'prop1': 'integer', + 'prop2': 'string' + }, + 'required': ['prop1','prop3','prop4'] + })"; + EXPECT_FALSE(obj_type.FromJson(CreateDictionaryValue(type_str).get(), nullptr, + &error)); + EXPECT_EQ(errors::commands::kUnknownProperty, error->GetCode()); + error.reset(); + + type_str = R"({ + 'properties': { + 'prop1': 'integer', + 'prop2': 'string' + }, + 'required': 'prop1' + })"; + EXPECT_FALSE(obj_type.FromJson(CreateDictionaryValue(type_str).get(), nullptr, + &error)); + EXPECT_EQ(errors::commands::kInvalidObjectSchema, error->GetCode()); + error.reset(); +} + +TEST(CommandSchema, ObjectSchema_UseRequired) { + ObjectPropType prop; + auto schema_str = R"({ + 'properties':{ + 'param1':'integer', + 'param2':'integer', + 'param3':{'default':3}, + 'param4':{'default':4} + }, + 'required':['param1','param3'] + })"; + ASSERT_TRUE( + prop.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, nullptr)); + + auto val_json = R"({ + 'param1':10, + 'param2':20, + 'param3':30, + 'param4':40 + })"; + auto value = + prop.CreatePropValue(*CreateDictionaryValue(val_json).get(), nullptr); + ASSERT_NE(nullptr, value); + ValueMap obj = value->GetObject()->GetValue(); + EXPECT_EQ(10, obj["param1"]->GetInt()->GetValue()); + EXPECT_EQ(20, obj["param2"]->GetInt()->GetValue()); + EXPECT_EQ(30, obj["param3"]->GetInt()->GetValue()); + EXPECT_EQ(40, obj["param4"]->GetInt()->GetValue()); + + val_json = "{'param1':100}"; + value = prop.CreatePropValue(*CreateDictionaryValue(val_json).get(), nullptr); + ASSERT_NE(nullptr, value); + obj = value->GetObject()->GetValue(); + EXPECT_EQ(3, obj.size()); + + EXPECT_EQ(100, obj["param1"]->GetInt()->GetValue()); + EXPECT_EQ(obj.end(), obj.find("param2")); + EXPECT_EQ(3, obj["param3"]->GetInt()->GetValue()); + EXPECT_EQ(4, obj["param4"]->GetInt()->GetValue()); +} + +TEST(CommandSchema, ObjectSchema_UseRequired_Failure) { + ObjectPropType prop; + auto schema_str = R"({ + 'properties':{ + 'param1':'integer', + 'param2':'integer', + 'param3':{'default':3}, + 'param4':{'default':4} + }, + 'required':['param1','param3'] + })"; + ASSERT_TRUE( + prop.FromJson(CreateDictionaryValue(schema_str).get(), nullptr, nullptr)); + + auto val_json = "{'param2':20}"; + ErrorPtr error; + auto value = + prop.CreatePropValue(*CreateDictionaryValue(val_json).get(), &error); + ASSERT_EQ(nullptr, value); + EXPECT_EQ(errors::commands::kPropertyMissing, error->GetCode()); +} + +} // namespace weave
diff --git a/src/commands/prop_constraints.cc b/src/commands/prop_constraints.cc new file mode 100644 index 0000000..b7e9cf6 --- /dev/null +++ b/src/commands/prop_constraints.cc
@@ -0,0 +1,202 @@ +// 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/prop_constraints.h" + +#include <base/json/json_writer.h> +#include <base/logging.h> + +#include "src/commands/prop_values.h" +#include "src/commands/schema_constants.h" +#include "src/string_utils.h" + +namespace weave { + +namespace { + +// Helper function to convert a property value to string, which is used for +// error reporting. +std::string PropValueToString(const PropValue& value) { + std::string result; + auto json = value.ToJson(); + CHECK(json); + base::JSONWriter::Write(*json, &result); + return result; +} + +} // anonymous namespace + +// Constraint ---------------------------------------------------------------- +Constraint::~Constraint() {} + +bool Constraint::ReportErrorLessThan(ErrorPtr* error, + const std::string& val, + const std::string& limit) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kOutOfRange, + "Value %s is out of range. It must not be less than %s", + val.c_str(), limit.c_str()); + return false; +} + +bool Constraint::ReportErrorGreaterThan(ErrorPtr* error, + const std::string& val, + const std::string& limit) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kOutOfRange, + "Value %s is out of range. It must not be greater than %s", + val.c_str(), limit.c_str()); + return false; +} + +bool Constraint::ReportErrorNotOneOf(ErrorPtr* error, + const std::string& val, + const std::vector<std::string>& values) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kOutOfRange, + "Value %s is invalid. Expected one of [%s]", val.c_str(), + Join(",", values).c_str()); + return false; +} + +void Constraint::AddToJsonDict(base::DictionaryValue* dict, + bool overridden_only) const { + if (!overridden_only || HasOverriddenAttributes()) { + auto value = ToJson(); + CHECK(value); + dict->SetWithoutPathExpansion(GetDictKey(), value.release()); + } +} + +// ConstraintStringLength ----------------------------------------------------- +ConstraintStringLength::ConstraintStringLength( + const InheritableAttribute<int>& limit) + : limit_(limit) {} +ConstraintStringLength::ConstraintStringLength(int limit) : limit_(limit) {} + +bool ConstraintStringLength::HasOverriddenAttributes() const { + return !limit_.is_inherited; +} + +std::unique_ptr<base::Value> ConstraintStringLength::ToJson() const { + return TypedValueToJson(limit_.value); +} + +// ConstraintStringLengthMin -------------------------------------------------- +ConstraintStringLengthMin::ConstraintStringLengthMin( + const InheritableAttribute<int>& limit) + : ConstraintStringLength(limit) {} +ConstraintStringLengthMin::ConstraintStringLengthMin(int limit) + : ConstraintStringLength(limit) {} + +bool ConstraintStringLengthMin::Validate(const PropValue& value, + ErrorPtr* error) const { + CHECK(value.GetString()) << "Expecting a string value for this constraint"; + const std::string& str = value.GetString()->GetValue(); + int length = static_cast<int>(str.size()); + if (length < limit_.value) { + if (limit_.value == 1) { + Error::AddTo(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kOutOfRange, "String must not be empty"); + } else { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kOutOfRange, + "String must be at least %d characters long," + " actual length of string '%s' is %d", + limit_.value, str.c_str(), length); + } + return false; + } + return true; +} + +std::unique_ptr<Constraint> ConstraintStringLengthMin::Clone() const { + return std::unique_ptr<Constraint>{new ConstraintStringLengthMin{limit_}}; +} + +std::unique_ptr<Constraint> ConstraintStringLengthMin::CloneAsInherited() + const { + return std::unique_ptr<Constraint>{ + new ConstraintStringLengthMin{limit_.value}}; +} + +// ConstraintStringLengthMax -------------------------------------------------- +ConstraintStringLengthMax::ConstraintStringLengthMax( + const InheritableAttribute<int>& limit) + : ConstraintStringLength(limit) {} +ConstraintStringLengthMax::ConstraintStringLengthMax(int limit) + : ConstraintStringLength(limit) {} + +bool ConstraintStringLengthMax::Validate(const PropValue& value, + ErrorPtr* error) const { + CHECK(value.GetString()) << "Expecting a string value for this constraint"; + const std::string& str = value.GetString()->GetValue(); + int length = static_cast<int>(str.size()); + if (length > limit_.value) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kOutOfRange, + "String must be no more than %d character(s) " + "long, actual length of string '%s' is %d", + limit_.value, str.c_str(), length); + return false; + } + return true; +} + +std::unique_ptr<Constraint> ConstraintStringLengthMax::Clone() const { + return std::unique_ptr<Constraint>{new ConstraintStringLengthMax{limit_}}; +} + +std::unique_ptr<Constraint> ConstraintStringLengthMax::CloneAsInherited() + const { + return std::unique_ptr<Constraint>{ + new ConstraintStringLengthMax{limit_.value}}; +} + +// ConstraintOneOf -------------------------------------------------- +ConstraintOneOf::ConstraintOneOf(InheritableAttribute<ValueVector> set) + : set_(std::move(set)) {} +ConstraintOneOf::ConstraintOneOf(ValueVector set) : set_(std::move(set)) {} + +bool ConstraintOneOf::Validate(const PropValue& value, ErrorPtr* error) const { + for (const auto& item : set_.value) { + if (value.IsEqual(item.get())) + return true; + } + std::vector<std::string> choice_list; + choice_list.reserve(set_.value.size()); + for (const auto& item : set_.value) { + choice_list.push_back(PropValueToString(*item)); + } + return ReportErrorNotOneOf(error, PropValueToString(value), choice_list); +} + +std::unique_ptr<Constraint> ConstraintOneOf::Clone() const { + InheritableAttribute<ValueVector> attr; + attr.is_inherited = set_.is_inherited; + attr.value.reserve(set_.value.size()); + for (const auto& prop_value : set_.value) { + attr.value.push_back(prop_value->Clone()); + } + return std::unique_ptr<Constraint>{new ConstraintOneOf{std::move(attr)}}; +} + +std::unique_ptr<Constraint> ConstraintOneOf::CloneAsInherited() const { + ValueVector cloned; + cloned.reserve(set_.value.size()); + for (const auto& prop_value : set_.value) { + cloned.push_back(prop_value->Clone()); + } + return std::unique_ptr<Constraint>{new ConstraintOneOf{std::move(cloned)}}; +} + +std::unique_ptr<base::Value> ConstraintOneOf::ToJson() const { + return TypedValueToJson(set_.value); +} + +const char* ConstraintOneOf::GetDictKey() const { + return commands::attributes::kOneOf_Enum; +} + +} // namespace weave
diff --git a/src/commands/prop_constraints.h b/src/commands/prop_constraints.h new file mode 100644 index 0000000..53a4d93 --- /dev/null +++ b/src/commands/prop_constraints.h
@@ -0,0 +1,315 @@ +// 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. + +#ifndef LIBWEAVE_SRC_COMMANDS_PROP_CONSTRAINTS_H_ +#define LIBWEAVE_SRC_COMMANDS_PROP_CONSTRAINTS_H_ + +#include <string> +#include <type_traits> +#include <vector> + +#include <base/macros.h> +#include <base/values.h> +#include <weave/error.h> + +#include "src/commands/prop_values.h" +#include "src/commands/schema_constants.h" +#include "src/commands/schema_utils.h" +#include "src/string_utils.h" + +namespace weave { + +enum class ConstraintType { Min, Max, StringLengthMin, StringLengthMax, OneOf }; + +// Abstract base class for all parameter constraints. Many constraints are +// type-dependent. Thus, a numeric parameter could have "minimum" and/or +// "maximum" constraints specified. Some constraints, such as "OneOf" apply to +// any data type. +class Constraint { + public: + Constraint() = default; + virtual ~Constraint(); + + // Gets the constraint type. + virtual ConstraintType GetType() const = 0; + + // Checks if any of the constraint properties/attributes are overridden + // from their base schema definition. If the constraint is inherited, then + // it will not be written to JSON when saving partial schema. + virtual bool HasOverriddenAttributes() const = 0; + + // Validates a parameter against the constraint. Returns true if parameter + // value satisfies the constraint, otherwise fills the optional |error| with + // the details for the failure. + virtual bool Validate(const PropValue& value, ErrorPtr* error) const = 0; + + // Makes a full copy of this Constraint instance. + virtual std::unique_ptr<Constraint> Clone() const = 0; + + // Makes a copy of the constraint object, marking all the attributes + // as inherited from the original definition. + virtual std::unique_ptr<Constraint> CloneAsInherited() const = 0; + + // Saves the constraint into the specified JSON |dict| object, representing + // the object schema. If |overridden_only| is set to true, then the + // inherited constraints will not be added to the schema object. + virtual void AddToJsonDict(base::DictionaryValue* dict, + bool overridden_only) const; + + // Saves the value of constraint to JSON value. E.g., if the numeric + // constraint was defined as {"minimum":20} this will create a JSON value + // of 20. The current design implies that each constraint has one value + // only. If this assumption changes, this interface needs to be updated + // accordingly. + virtual std::unique_ptr<base::Value> ToJson() const = 0; + + // Overloaded by the concrete class implementation, it should return the + // JSON object property name to store the constraint's value as. + // E.g., if the numeric constraint was defined as {"minimum":20} this + // method should return "minimum". + virtual const char* GetDictKey() const = 0; + + protected: + // Static helper methods to format common constraint validation errors. + // They fill the |error| object with specific error message. + // Since these functions could be used by constraint objects for various + // data types, the values used in validation are expected to be + // send as strings already. + static bool ReportErrorLessThan(ErrorPtr* error, + const std::string& val, + const std::string& limit); + static bool ReportErrorGreaterThan(ErrorPtr* error, + const std::string& val, + const std::string& limit); + + static bool ReportErrorNotOneOf(ErrorPtr* error, + const std::string& val, + const std::vector<std::string>& values); + + private: + DISALLOW_COPY_AND_ASSIGN(Constraint); +}; + +// ConstraintMinMaxBase is a base class for numeric Minimum and Maximum +// constraints. +template <typename T> +class ConstraintMinMaxBase : public Constraint { + public: + explicit ConstraintMinMaxBase(const InheritableAttribute<T>& limit) + : limit_(limit) {} + explicit ConstraintMinMaxBase(const T& limit) : limit_(limit) {} + + // Implementation of Constraint::HasOverriddenAttributes(). + bool HasOverriddenAttributes() const override { return !limit_.is_inherited; } + + // Implementation of Constraint::ToJson(). + std::unique_ptr<base::Value> ToJson() const override { + return TypedValueToJson(limit_.value); + } + + // Stores the upper/lower value limit for maximum/minimum constraint. + // |limit_.is_inherited| indicates whether the constraint is inherited + // from base schema or overridden. + InheritableAttribute<T> limit_; + + private: + DISALLOW_COPY_AND_ASSIGN(ConstraintMinMaxBase); +}; + +// Implementation of Minimum value constraint for Integer/Double types. +template <typename T> +class ConstraintMin : public ConstraintMinMaxBase<T> { + public: + explicit ConstraintMin(const InheritableAttribute<T>& limit) + : ConstraintMinMaxBase<T>(limit) {} + explicit ConstraintMin(const T& limit) : ConstraintMinMaxBase<T>(limit) {} + + // Implementation of Constraint::GetType(). + ConstraintType GetType() const override { return ConstraintType::Min; } + + // Implementation of Constraint::Validate(). + bool Validate(const PropValue& value, ErrorPtr* error) const override { + const T& v = static_cast<const TypedValueBase<T>&>(value).GetValue(); + if (v < this->limit_.value) { + return this->ReportErrorLessThan(error, std::to_string(v), + std::to_string(this->limit_.value)); + } + return true; + } + + // Implementation of Constraint::Clone(). + std::unique_ptr<Constraint> Clone() const override { + return std::unique_ptr<Constraint>{new ConstraintMin{this->limit_}}; + } + + // Implementation of Constraint::CloneAsInherited(). + std::unique_ptr<Constraint> CloneAsInherited() const override { + return std::unique_ptr<Constraint>{new ConstraintMin{this->limit_.value}}; + } + + // Implementation of Constraint::GetDictKey(). + const char* GetDictKey() const override { + return commands::attributes::kNumeric_Min; + } + + private: + DISALLOW_COPY_AND_ASSIGN(ConstraintMin); +}; + +// Implementation of Maximum value constraint for Integer/Double types. +template <typename T> +class ConstraintMax : public ConstraintMinMaxBase<T> { + public: + explicit ConstraintMax(const InheritableAttribute<T>& limit) + : ConstraintMinMaxBase<T>(limit) {} + explicit ConstraintMax(const T& limit) : ConstraintMinMaxBase<T>(limit) {} + + // Implementation of Constraint::GetType(). + ConstraintType GetType() const override { return ConstraintType::Max; } + + // Implementation of Constraint::Validate(). + bool Validate(const PropValue& value, ErrorPtr* error) const override { + const T& v = static_cast<const TypedValueBase<T>&>(value).GetValue(); + if (v > this->limit_.value) + return this->ReportErrorGreaterThan(error, std::to_string(v), + std::to_string(this->limit_.value)); + return true; + } + + // Implementation of Constraint::Clone(). + std::unique_ptr<Constraint> Clone() const override { + return std::unique_ptr<Constraint>{new ConstraintMax{this->limit_}}; + } + + // Implementation of Constraint::CloneAsInherited(). + std::unique_ptr<Constraint> CloneAsInherited() const override { + return std::unique_ptr<Constraint>{new ConstraintMax{this->limit_.value}}; + } + + // Implementation of Constraint::GetDictKey(). + const char* GetDictKey() const override { + return commands::attributes::kNumeric_Max; + } + + private: + DISALLOW_COPY_AND_ASSIGN(ConstraintMax); +}; + +// ConstraintStringLength is a base class for Minimum/Maximum string length +// constraints, similar to ConstraintMinMaxBase of numeric types. +class ConstraintStringLength : public Constraint { + public: + explicit ConstraintStringLength(const InheritableAttribute<int>& limit); + explicit ConstraintStringLength(int limit); + + // Implementation of Constraint::HasOverriddenAttributes(). + bool HasOverriddenAttributes() const override; + // Implementation of Constraint::ToJson(). + std::unique_ptr<base::Value> ToJson() const override; + + // Stores the upper/lower value limit for string length constraint. + // |limit_.is_inherited| indicates whether the constraint is inherited + // from base schema or overridden. + InheritableAttribute<int> limit_; + + private: + DISALLOW_COPY_AND_ASSIGN(ConstraintStringLength); +}; + +// Implementation of Minimum string length constraint. +class ConstraintStringLengthMin : public ConstraintStringLength { + public: + explicit ConstraintStringLengthMin(const InheritableAttribute<int>& limit); + explicit ConstraintStringLengthMin(int limit); + + // Implementation of Constraint::GetType(). + ConstraintType GetType() const override { + return ConstraintType::StringLengthMin; + } + + // Implementation of Constraint::Validate(). + bool Validate(const PropValue& value, ErrorPtr* error) const override; + + // Implementation of Constraint::Clone(). + std::unique_ptr<Constraint> Clone() const override; + + // Implementation of Constraint::CloneAsInherited(). + std::unique_ptr<Constraint> CloneAsInherited() const override; + // Implementation of Constraint::GetDictKey(). + const char* GetDictKey() const override { + return commands::attributes::kString_MinLength; + } + + private: + DISALLOW_COPY_AND_ASSIGN(ConstraintStringLengthMin); +}; + +// Implementation of Maximum string length constraint. +class ConstraintStringLengthMax : public ConstraintStringLength { + public: + explicit ConstraintStringLengthMax(const InheritableAttribute<int>& limit); + explicit ConstraintStringLengthMax(int limit); + + // Implementation of Constraint::GetType(). + ConstraintType GetType() const override { + return ConstraintType::StringLengthMax; + } + + // Implementation of Constraint::Validate(). + bool Validate(const PropValue& value, ErrorPtr* error) const override; + + // Implementation of Constraint::Clone(). + std::unique_ptr<Constraint> Clone() const override; + + // Implementation of Constraint::CloneAsInherited(). + std::unique_ptr<Constraint> CloneAsInherited() const override; + + // Implementation of Constraint::GetDictKey(). + const char* GetDictKey() const override { + return commands::attributes::kString_MaxLength; + } + + private: + DISALLOW_COPY_AND_ASSIGN(ConstraintStringLengthMax); +}; + +// Implementation of OneOf constraint for different data types. +class ConstraintOneOf : public Constraint { + public: + explicit ConstraintOneOf(InheritableAttribute<ValueVector> set); + explicit ConstraintOneOf(ValueVector set); + + // Implementation of Constraint::GetType(). + ConstraintType GetType() const override { return ConstraintType::OneOf; } + + // Implementation of Constraint::HasOverriddenAttributes(). + bool HasOverriddenAttributes() const override { return !set_.is_inherited; } + + // Implementation of Constraint::Validate(). + bool Validate(const PropValue& value, ErrorPtr* error) const override; + + // Implementation of Constraint::Clone(). + std::unique_ptr<Constraint> Clone() const override; + + // Implementation of Constraint::CloneAsInherited(). + std::unique_ptr<Constraint> CloneAsInherited() const override; + + // Implementation of Constraint::ToJson(). + std::unique_ptr<base::Value> ToJson() const override; + + // Implementation of Constraint::GetDictKey(). + const char* GetDictKey() const override; + + // Stores the list of acceptable values for the parameter. + // |set_.is_inherited| indicates whether the constraint is inherited + // from base schema or overridden. + InheritableAttribute<ValueVector> set_; + + private: + DISALLOW_COPY_AND_ASSIGN(ConstraintOneOf); +}; + +} // namespace weave + +#endif // LIBWEAVE_SRC_COMMANDS_PROP_CONSTRAINTS_H_
diff --git a/src/commands/prop_types.cc b/src/commands/prop_types.cc new file mode 100644 index 0000000..88a53bd --- /dev/null +++ b/src/commands/prop_types.cc
@@ -0,0 +1,653 @@ +// 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/prop_types.h" + +#include <algorithm> +#include <limits> +#include <set> + +#include <base/json/json_writer.h> +#include <base/logging.h> +#include <base/values.h> + +#include "src/commands/object_schema.h" +#include "src/commands/prop_values.h" +#include "src/commands/schema_constants.h" + +namespace weave { + +// PropType ------------------------------------------------------------------- +PropType::PropType() {} + +PropType::~PropType() {} + +std::string PropType::GetTypeAsString() const { + return GetTypeStringFromType(GetType()); +} + +bool PropType::HasOverriddenAttributes() const { + if (default_.value && !default_.is_inherited) + return true; + + for (const auto& pair : constraints_) { + if (pair.second->HasOverriddenAttributes()) + return true; + } + return false; +} + +bool PropType::IsRequired() const { + return required_.value; +} + +void PropType::MakeRequired(bool required) { + required_.value = required; + required_.is_inherited = false; +} + +std::unique_ptr<base::Value> PropType::ToJson(bool full_schema, + bool in_command_def) const { + // Determine if we need to output "isRequired" attribute. + const bool include_required = in_command_def && !required_.is_inherited; + + // If we must include "isRequired" attribute, then treat this as "full schema" + // request because there could be cases where we have just this attribute and + // won't be able to infer the type from the constraints only. + if (include_required) + full_schema = true; + + if (!full_schema && !HasOverriddenAttributes()) { + if (based_on_schema_) + return std::unique_ptr<base::Value>(new base::DictionaryValue); + return TypedValueToJson(GetTypeAsString()); + } + + 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(); + } + } + + for (const auto& pair : constraints_) + pair.second->AddToJsonDict(dict.get(), !full_schema); + + if (default_.value && (full_schema || !default_.is_inherited)) { + auto def_val = default_.value->ToJson(); + CHECK(def_val); + dict->Set(commands::attributes::kDefault, def_val.release()); + } + + if (include_required) + dict->SetBoolean(commands::attributes::kIsRequired, required_.value); + return std::unique_ptr<base::Value>(dict.release()); +} + +std::unique_ptr<PropType> PropType::Clone() const { + auto cloned = PropType::Create(GetType()); + cloned->based_on_schema_ = based_on_schema_; + for (const auto& pair : constraints_) { + cloned->constraints_.emplace(pair.first, pair.second->Clone()); + } + cloned->default_.is_inherited = default_.is_inherited; + if (default_.value) + cloned->default_.value = default_.value->Clone(); + cloned->required_ = required_; + return cloned; +} + +bool PropType::FromJson(const base::DictionaryValue* value, + const PropType* base_schema, + ErrorPtr* error) { + if (base_schema && base_schema->GetType() != GetType()) { + Error::AddToPrintf(error, FROM_HERE, 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(); + // Add the well-known object properties first (like "type", "displayName", + // "default") to the list of "processed" keys so we do not complain about them + // when we check for unknown/unexpected keys below. + std::set<std::string> processed_keys{ + commands::attributes::kType, commands::attributes::kDisplayName, + commands::attributes::kDefault, commands::attributes::kIsRequired, + }; + if (!ObjectSchemaFromJson(value, base_schema, &processed_keys, error)) + return false; + if (base_schema) { + for (const auto& pair : base_schema->GetConstraints()) { + constraints_.emplace(pair.first, pair.second->CloneAsInherited()); + } + } + 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()) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kUnknownProperty, + "Unexpected property '%s'", key.c_str()); + return false; + } + iter.Advance(); + } + + // Read the "isRequired" attribute, if specified. + bool required = false; + if (value->GetBoolean(commands::attributes::kIsRequired, &required)) { + required_.value = required; + required_.is_inherited = false; + } else if (base_schema) { + // If we have the base schema, inherit the type's required value from it. + if (base_schema->required_.value) + required_.value = base_schema->required_.value; + required_.is_inherited = true; + } + + // Read the default value, if specified. + // We need to do this last since the current type definition must be complete, + // so we can parse and validate the value of the default. + const base::Value* defval = nullptr; // Owned by value + if (value->GetWithoutPathExpansion(commands::attributes::kDefault, &defval)) { + std::unique_ptr<PropValue> prop_value = CreatePropValue(*defval, error); + if (!prop_value) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidPropValue, + "Invalid value for property '%s'", + commands::attributes::kDefault); + return false; + } + default_.value = std::move(prop_value); + default_.is_inherited = false; + } else if (base_schema) { + // If we have the base schema, inherit the type's default value from it. + // It doesn't matter if the base schema actually has a default value + // specified or not. If it doesn't, then the current type definition will + // have no default value set either (|default_.value| is a unique_ptr to + // PropValue, which can be set to nullptr). + if (base_schema->default_.value) + default_.value = base_schema->default_.value->Clone(); + default_.is_inherited = true; + } + return true; +} + +void PropType::AddConstraint(std::unique_ptr<Constraint> constraint) { + constraints_[constraint->GetType()] = std::move(constraint); +} + +void PropType::RemoveConstraint(ConstraintType constraint_type) { + constraints_.erase(constraint_type); +} + +void PropType::RemoveAllConstraints() { + constraints_.clear(); +} + +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::ValidateConstraints(const PropValue& value, + 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"}, {ValueType::Array, "array"}, + }; + 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 ValueType::Int: + prop = new IntPropType; + break; + case ValueType::Double: + prop = new DoublePropType; + break; + case ValueType::String: + prop = new StringPropType; + break; + case ValueType::Boolean: + prop = new BooleanPropType; + break; + case ValueType::Object: + prop = new ObjectPropType; + break; + case ValueType::Array: + prop = new ArrayPropType; + break; + } + return std::unique_ptr<PropType>(prop); +} + +template <typename T> +static std::unique_ptr<Constraint> LoadOneOfConstraint( + const base::DictionaryValue* value, + const PropType* prop_type, + ErrorPtr* error) { + std::unique_ptr<Constraint> constraint; + const base::Value* list = nullptr; // Owned by |value| + CHECK(value->Get(commands::attributes::kOneOf_Enum, &list)) + << "'enum' property missing in JSON dictionary"; + ValueVector choice_list; + ArrayPropType array_type; + array_type.SetItemType(prop_type->Clone()); + if (!TypedValueFromJson(list, &array_type, &choice_list, error)) + return constraint; + InheritableAttribute<ValueVector> val(std::move(choice_list), false); + constraint.reset(new ConstraintOneOf{std::move(val)}); + return constraint; +} + +template <class ConstraintClass, typename T> +static std::unique_ptr<Constraint> LoadMinMaxConstraint( + const char* dict_key, + const base::DictionaryValue* value, + ErrorPtr* error) { + std::unique_ptr<Constraint> constraint; + 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, nullptr, &limit.value, error)) + return constraint; + limit.is_inherited = false; + + constraint.reset(new ConstraintClass{limit}); + return constraint; +} + +// PropTypeBase ---------------------------------------------------------------- + +template <class Derived, class Value, typename T> +bool PropTypeBase<Derived, Value, T>::ConstraintsFromJson( + const base::DictionaryValue* value, + std::set<std::string>* processed_keys, + ErrorPtr* error) { + if (!PropType::ConstraintsFromJson(value, processed_keys, error)) + return false; + + if (value->HasKey(commands::attributes::kOneOf_Enum)) { + auto type = Clone(); + type->RemoveAllConstraints(); + auto constraint = LoadOneOfConstraint<T>(value, type.get(), error); + if (!constraint) + return false; + this->AddConstraint(std::move(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, + 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, error); + if (!constraint) + return false; + this->AddConstraint(std::move(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, error); + if (!constraint) + return false; + this->AddConstraint(std::move(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, + 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, error); + if (!constraint) + return false; + AddConstraint(std::move(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, error); + if (!constraint) + return false; + AddConstraint(std::move(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::unique_ptr<ConstraintStringLengthMin>{ + new ConstraintStringLengthMin{min_attr}}); + AddConstraint(std::unique_ptr<ConstraintStringLengthMax>{ + new 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_{ObjectSchema::Create(), false} {} + +bool ObjectPropType::HasOverriddenAttributes() const { + return PropType::HasOverriddenAttributes() || !object_schema_.is_inherited; +} + +std::unique_ptr<PropType> ObjectPropType::Clone() const { + auto cloned = Base::Clone(); + + cloned->GetObject()->object_schema_.is_inherited = + object_schema_.is_inherited; + cloned->GetObject()->object_schema_.value = object_schema_.value->Clone(); + return cloned; +} + +std::unique_ptr<base::Value> ObjectPropType::ToJson(bool full_schema, + bool in_command_def) const { + std::unique_ptr<base::Value> value = + PropType::ToJson(full_schema, in_command_def); + CHECK(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, false); + CHECK(object_schema); + + dict->SetWithoutPathExpansion(commands::attributes::kObject_Properties, + object_schema.release()); + dict->SetBooleanWithoutPathExpansion( + commands::attributes::kObject_AdditionalProperties, + object_schema_.value->GetExtraPropertiesAllowed()); + std::unique_ptr<base::ListValue> required{new base::ListValue}; + for (const auto& pair : object_schema_.value->GetProps()) { + if (pair.second->IsRequired()) + required->AppendString(pair.first); + } + if (required->GetSize() > 0) { + dict->Set(commands::attributes::kObject_Required, required.release()); + } + } + return value; +} + +bool ObjectPropType::ObjectSchemaFromJson(const base::DictionaryValue* value, + const PropType* base_schema, + std::set<std::string>* processed_keys, + ErrorPtr* error) { + if (!Base::ObjectSchemaFromJson(value, base_schema, processed_keys, error)) + return false; + + using commands::attributes::kObject_Properties; + using commands::attributes::kObject_AdditionalProperties; + + const ObjectSchema* base_object_schema = nullptr; + if (base_schema) + base_object_schema = base_schema->GetObject()->GetObjectSchemaPtr(); + + const base::DictionaryValue* props = nullptr; + std::unique_ptr<ObjectSchema> object_schema; + bool inherited = false; + if (value->GetDictionaryWithoutPathExpansion(kObject_Properties, &props)) { + processed_keys->insert(kObject_Properties); + object_schema.reset(new ObjectSchema); + if (!object_schema->FromJson(props, base_object_schema, error)) { + Error::AddTo(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidObjectSchema, + "Error parsing object property schema"); + return false; + } + } else if (base_object_schema) { + object_schema = base_object_schema->Clone(); + inherited = true; + } else { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidObjectSchema, + "Object type definition must include the " + "object schema ('%s' field not found)", + kObject_Properties); + return false; + } + bool extra_properties_allowed = false; + if (value->GetBooleanWithoutPathExpansion(kObject_AdditionalProperties, + &extra_properties_allowed)) { + processed_keys->insert(kObject_AdditionalProperties); + object_schema->SetExtraPropertiesAllowed(extra_properties_allowed); + inherited = false; + } + const base::Value* required = nullptr; + if (value->Get(commands::attributes::kObject_Required, &required)) { + processed_keys->insert(commands::attributes::kObject_Required); + const base::ListValue* required_list = nullptr; + if (!required->GetAsList(&required_list)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidObjectSchema, + "Property '%s' must be an array", + commands::attributes::kObject_Required); + return false; + } + for (const base::Value* value : *required_list) { + std::string name; + if (!value->GetAsString(&name)) { + std::string json_value; + CHECK(base::JSONWriter::Write(*value, &json_value)); + Error::AddToPrintf( + error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidObjectSchema, + "Property '%s' contains invalid element (%s). String expected", + commands::attributes::kObject_Required, json_value.c_str()); + return false; + } + if (!object_schema->MarkPropRequired(name, error)) + return false; + inherited = false; + } + } + object_schema_.value = std::move(object_schema); + object_schema_.is_inherited = inherited; + + return true; +} + +void ObjectPropType::SetObjectSchema( + std::unique_ptr<const ObjectSchema> schema) { + object_schema_.value = std::move(schema); + object_schema_.is_inherited = false; +} + +// ArrayPropType ------------------------------------------------------------- + +ArrayPropType::ArrayPropType() {} + +bool ArrayPropType::HasOverriddenAttributes() const { + return PropType::HasOverriddenAttributes() || !item_type_.is_inherited; +} + +std::unique_ptr<PropType> ArrayPropType::Clone() const { + auto cloned = Base::Clone(); + + cloned->GetArray()->item_type_.is_inherited = item_type_.is_inherited; + cloned->GetArray()->item_type_.value = item_type_.value->Clone(); + return cloned; +} + +std::unique_ptr<base::Value> ArrayPropType::ToJson(bool full_schema, + bool in_command_def) const { + std::unique_ptr<base::Value> value = + PropType::ToJson(full_schema, in_command_def); + CHECK(value); + if (!item_type_.is_inherited || full_schema) { + base::DictionaryValue* dict = nullptr; + CHECK(value->GetAsDictionary(&dict)) << "Expecting a JSON object"; + auto type = item_type_.value->ToJson(full_schema, false); + CHECK(type); + dict->SetWithoutPathExpansion(commands::attributes::kItems, type.release()); + } + return value; +} + +bool ArrayPropType::ObjectSchemaFromJson(const base::DictionaryValue* value, + const PropType* base_schema, + std::set<std::string>* processed_keys, + ErrorPtr* error) { + if (!Base::ObjectSchemaFromJson(value, base_schema, processed_keys, error)) + return false; + + using commands::attributes::kItems; + + const PropType* base_type = nullptr; + if (base_schema) + base_type = base_schema->GetArray()->GetItemTypePtr(); + + const base::Value* type_value = nullptr; + if (value->GetWithoutPathExpansion(kItems, &type_value)) { + processed_keys->insert(kItems); + auto item_type = ObjectSchema::PropFromJson(*type_value, base_type, error); + if (!item_type) + return false; + if (item_type->GetType() == ValueType::Array) { + Error::AddTo(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidObjectSchema, + "Arrays of arrays are not supported"); + return false; + } + SetItemType(std::move(item_type)); + } else if (!item_type_.value) { + if (base_type) { + item_type_.value = base_type->Clone(); + item_type_.is_inherited = true; + } else { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidObjectSchema, + "Array type definition must include the " + "array item type ('%s' field not found)", + kItems); + return false; + } + } + return true; +} + +void ArrayPropType::SetItemType(std::unique_ptr<const PropType> item_type) { + item_type_.value = std::move(item_type); + item_type_.is_inherited = false; +} + +} // namespace weave
diff --git a/src/commands/prop_types.h b/src/commands/prop_types.h new file mode 100644 index 0000000..f784eb6 --- /dev/null +++ b/src/commands/prop_types.h
@@ -0,0 +1,352 @@ +// 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. + +#ifndef LIBWEAVE_SRC_COMMANDS_PROP_TYPES_H_ +#define LIBWEAVE_SRC_COMMANDS_PROP_TYPES_H_ + +#include <limits> +#include <map> +#include <memory> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include <weave/error.h> + +#include "src/commands/prop_constraints.h" +#include "src/commands/prop_values.h" + +namespace weave { + +class IntPropType; +class DoublePropType; +class StringPropType; +class BooleanPropType; +class ObjectPropType; +class ArrayPropType; + +// PropType is a base class for all parameter type definition objects. +// Property definitions of a particular type will derive from this class and +// provide type-specific implementations. +class PropType { + public: + // ConstraintMap is a type alias for a map containing parameter + // constraints. It is implemented as a map for fast look-ups of constraints + // of particular type. Also it is expected to have at most one constraint + // of each type (e.g. it makes no sense to impose two "minimum" constraints + // onto a numeric parameter). + using ConstraintMap = std::map<ConstraintType, std::unique_ptr<Constraint>>; + + PropType(); + virtual ~PropType(); + + // Gets the parameter type as an enumeration. + virtual ValueType GetType() const = 0; + // Gets the parameter type as a string. + std::string GetTypeAsString() const; + // Returns true if this parameter definition inherits a type + // definition from a base object schema. + bool IsBasedOnSchema() const { return based_on_schema_; } + // Returns a default value specified for the type, or nullptr if no default + // is available. + const PropValue* GetDefaultValue() const { return default_.value.get(); } + // Gets the constraints specified for the parameter, if any. + const ConstraintMap& GetConstraints() const { return constraints_; } + // Returns true if this value is required. Properties are marked as required + // by using "isRequired" attribute or listed in "required" array. + bool IsRequired() const; + // Sets the required attribute to the value of |required| and marks it as + // overridden (not-inherited). + void MakeRequired(bool required); + // Checks if any of the type attributes were overridden from the base + // schema definition. If this type does not inherit from a base schema, + // this method returns true. + // An attribute could be the value of any of the constraints, default + // value of a parameter or any other data that may be specified in + // parameter type definition in and can be inherited from the base schema. + virtual bool HasOverriddenAttributes() const; + + // Type conversion methods. Used in lieu of RTTI and dynamic_cast<>. + virtual IntPropType* GetInt() { return nullptr; } + virtual IntPropType const* GetInt() const { return nullptr; } + virtual DoublePropType* GetDouble() { return nullptr; } + virtual DoublePropType const* GetDouble() const { return nullptr; } + virtual StringPropType* GetString() { return nullptr; } + virtual StringPropType const* GetString() const { return nullptr; } + virtual BooleanPropType* GetBoolean() { return nullptr; } + virtual BooleanPropType const* GetBoolean() const { return nullptr; } + virtual ObjectPropType* GetObject() { return nullptr; } + virtual ObjectPropType const* GetObject() const { return nullptr; } + virtual ArrayPropType* GetArray() { return nullptr; } + virtual ArrayPropType const* GetArray() const { return nullptr; } + + // Makes a full copy of this type definition. + virtual std::unique_ptr<PropType> Clone() const; + + // Creates an instance of associated value object. + virtual std::unique_ptr<PropValue> CreatePropValue(const base::Value& value, + ErrorPtr* error) const = 0; + + // Saves the parameter type definition as a JSON object. + // If |full_schema| is set to true, the full type definition is saved, + // otherwise only the overridden properties and attributes from the base + // schema is saved. That is, inherited properties and attributes are not + // saved. + // If it fails, returns "nullptr" and fills in the |error| with additional + // error information. + // |in_command_def| is set to true if the property type describes a + // GCD command parameter, otherwise it is for an object property. + // Command definitions handle required parameters differently (using + // "isRequired" property as opposed to "required" list for object properties). + virtual std::unique_ptr<base::Value> ToJson(bool full_schema, + bool in_command_def) const; + // Parses an JSON parameter type definition. Optional |base_schema| may + // specify the base schema type definition this type should be based upon. + // If not specified (nullptr), the parameter type is assumed to be a full + // definition and any omitted required properties are treated as an error. + // Returns true on success, otherwise fills in the |error| with additional + // error information. + virtual bool FromJson(const base::DictionaryValue* value, + const PropType* base_schema, + ErrorPtr* error); + // Helper function to load object schema from JSON. + virtual bool ObjectSchemaFromJson(const base::DictionaryValue* value, + const PropType* base_schema, + std::set<std::string>* processed_keys, + ErrorPtr* error) { + return true; + } + // Helper function to load type-specific constraints from JSON. + virtual bool ConstraintsFromJson(const base::DictionaryValue* value, + std::set<std::string>* processed_keys, + ErrorPtr* error) { + return true; + } + + // Additional helper static methods to help with converting a type enum + // value into a string and back. + using TypeMap = std::vector<std::pair<ValueType, std::string>>; + // Returns a list of value types and corresponding type names. + static const TypeMap& GetTypeMap(); + // Gets the type name string for the given type. + static std::string GetTypeStringFromType(ValueType type); + // Finds the type for the given type name. Returns true on success. + static bool GetTypeFromTypeString(const std::string& name, ValueType* type); + + // Creates an instance of PropType-derived class for the specified + // parameter type. + static std::unique_ptr<PropType> Create(ValueType type); + + // Adds a constraint to the type definition. + void AddConstraint(std::unique_ptr<Constraint> constraint); + // Removes a constraint of given type, if it exists. + void RemoveConstraint(ConstraintType constraint_type); + // Removes all constraints. + void RemoveAllConstraints(); + + // Finds a constraint of given type. Returns nullptr if not found. + const Constraint* GetConstraint(ConstraintType constraint_type) const; + Constraint* GetConstraint(ConstraintType constraint_type); + + // Validates the given value against all the constraints. + bool ValidateConstraints(const PropValue& value, ErrorPtr* error) const; + + protected: + friend class StatePackage; + virtual std::unique_ptr<PropValue> CreateDefaultValue() const = 0; + + // Specifies if this parameter definition is derived from a base + // object schema. + bool based_on_schema_ = false; + // A list of constraints specified for the parameter. + ConstraintMap constraints_; + // The default value specified for the parameter, if any. If the default + // value is present, the parameter is treated as optional and the default + // value is used if the parameter value is omitted when sending a command. + // Otherwise the parameter is treated as required and, if it is omitted, + // this is treated as an error. + InheritableAttribute<std::unique_ptr<PropValue>> default_; + // Specifies whether the parameter/property is required and must be specified + // (either directly, or by the default value being provided in the schema). + // Non-required parameters can be omitted completely and their values will not + // be present in the object instance. + InheritableAttribute<bool> required_; +}; + +// Base class for all the derived concrete implementations of property +// type classes. Provides implementations for common methods of PropType base. +template <class Derived, class Value, typename T> +class PropTypeBase : public PropType { + public: + // Overrides from PropType. + ValueType GetType() const override { return GetValueType<T>(); } + + std::unique_ptr<PropValue> CreatePropValue(const base::Value& value, + ErrorPtr* error) const override { + return CreateValue(value, error); + } + + std::unique_ptr<Value> CreateValue(const base::Value& value, + ErrorPtr* error) const { + return Value::CreateFromJson(value, *this, error); + } + + bool ConstraintsFromJson(const base::DictionaryValue* value, + std::set<std::string>* processed_keys, + ErrorPtr* error) override; + + protected: + std::unique_ptr<PropValue> CreateDefaultValue() const override { + if (GetDefaultValue()) + return GetDefaultValue()->Clone(); + return std::unique_ptr<PropValue>{new Value{*this}}; + } +}; + +// Helper base class for Int and Double parameter types. +template <class Derived, class Value, typename T> +class NumericPropTypeBase : public PropTypeBase<Derived, Value, T> { + public: + using Base = PropTypeBase<Derived, Value, T>; + bool ConstraintsFromJson(const base::DictionaryValue* value, + std::set<std::string>* processed_keys, + ErrorPtr* error) override; + + // Helper method to set and obtain a min/max constraint values. + // Used mostly for unit testing. + void AddMinMaxConstraint(T min_value, T max_value) { + InheritableAttribute<T> min_attr(min_value, false); + InheritableAttribute<T> max_attr(max_value, false); + this->AddConstraint( + std::unique_ptr<ConstraintMin<T>>{new ConstraintMin<T>{min_attr}}); + this->AddConstraint( + std::unique_ptr<ConstraintMax<T>>{new ConstraintMax<T>{max_attr}}); + } + T GetMinValue() const { + auto mmc = static_cast<const ConstraintMin<T>*>( + this->GetConstraint(ConstraintType::Min)); + return mmc ? mmc->limit_.value : std::numeric_limits<T>::lowest(); + } + T GetMaxValue() const { + auto mmc = static_cast<const ConstraintMax<T>*>( + this->GetConstraint(ConstraintType::Max)); + return mmc ? mmc->limit_.value : (std::numeric_limits<T>::max)(); + } +}; + +// Property definition of Integer type. +class IntPropType : public NumericPropTypeBase<IntPropType, IntValue, int> { + public: + // Overrides from the PropType base class. + IntPropType* GetInt() override { return this; } + IntPropType const* GetInt() const override { return this; } +}; + +// Property definition of Number type. +class DoublePropType + : public NumericPropTypeBase<DoublePropType, DoubleValue, double> { + public: + // Overrides from the PropType base class. + DoublePropType* GetDouble() override { return this; } + DoublePropType const* GetDouble() const override { return this; } +}; + +// Property definition of String type. +class StringPropType + : public PropTypeBase<StringPropType, StringValue, std::string> { + public: + using Base = PropTypeBase<StringPropType, StringValue, std::string>; + // Overrides from the PropType base class. + StringPropType* GetString() override { return this; } + StringPropType const* GetString() const override { return this; } + + bool ConstraintsFromJson(const base::DictionaryValue* value, + std::set<std::string>* processed_keys, + ErrorPtr* error) override; + + // Helper methods to add and inspect simple constraints. + // Used mostly for unit testing. + void AddLengthConstraint(int min_len, int max_len); + int GetMinLength() const; + int GetMaxLength() const; +}; + +// Property definition of Boolean type. +class BooleanPropType + : public PropTypeBase<BooleanPropType, BooleanValue, bool> { + public: + // Overrides from the PropType base class. + BooleanPropType* GetBoolean() override { return this; } + BooleanPropType const* GetBoolean() const override { return this; } +}; + +// Parameter definition of Object type. +class ObjectPropType + : public PropTypeBase<ObjectPropType, ObjectValue, ValueMap> { + public: + using Base = PropTypeBase<ObjectPropType, ObjectValue, ValueMap>; + ObjectPropType(); + + // Overrides from the ParamType base class. + bool HasOverriddenAttributes() const override; + + ObjectPropType* GetObject() override { return this; } + ObjectPropType const* GetObject() const override { return this; } + + std::unique_ptr<PropType> Clone() const override; + + std::unique_ptr<base::Value> ToJson(bool full_schema, + bool in_command_def) const override; + bool ObjectSchemaFromJson(const base::DictionaryValue* value, + const PropType* base_schema, + std::set<std::string>* processed_keys, + ErrorPtr* error) override; + + // Returns a schema for Object-type parameter. + inline const ObjectSchema* GetObjectSchemaPtr() const { + return object_schema_.value.get(); + } + void SetObjectSchema(std::unique_ptr<const ObjectSchema> schema); + + private: + InheritableAttribute<std::unique_ptr<const ObjectSchema>> object_schema_; +}; + +// Parameter definition of Array type. +class ArrayPropType + : public PropTypeBase<ArrayPropType, ArrayValue, ValueVector> { + public: + using Base = PropTypeBase<ArrayPropType, ArrayValue, ValueVector>; + ArrayPropType(); + + // Overrides from the ParamType base class. + bool HasOverriddenAttributes() const override; + + ArrayPropType* GetArray() override { return this; } + ArrayPropType const* GetArray() const override { return this; } + + std::unique_ptr<PropType> Clone() const override; + + std::unique_ptr<base::Value> ToJson(bool full_schema, + bool in_command_def) const override; + + bool ObjectSchemaFromJson(const base::DictionaryValue* value, + const PropType* base_schema, + std::set<std::string>* processed_keys, + ErrorPtr* error) override; + + // Returns a type for Array elements. + inline const PropType* GetItemTypePtr() const { + return item_type_.value.get(); + } + void SetItemType(std::unique_ptr<const PropType> item_type); + + private: + InheritableAttribute<std::unique_ptr<const PropType>> item_type_; +}; + +} // namespace weave + +#endif // LIBWEAVE_SRC_COMMANDS_PROP_TYPES_H_
diff --git a/src/commands/prop_values.cc b/src/commands/prop_values.cc new file mode 100644 index 0000000..6f30bee --- /dev/null +++ b/src/commands/prop_values.cc
@@ -0,0 +1,17 @@ +// 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/prop_values.h" + +#include "src/commands/prop_types.h" + +namespace weave { + +PropValue::PropValue(const PropType& type) : type_{type.Clone()} {} + +PropValue::PropValue(const PropValue& other) : PropValue{*other.type_} {} + +PropValue::~PropValue() {} + +} // namespace weave
diff --git a/src/commands/prop_values.h b/src/commands/prop_values.h new file mode 100644 index 0000000..f0a401e --- /dev/null +++ b/src/commands/prop_values.h
@@ -0,0 +1,250 @@ +// 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. + +#ifndef LIBWEAVE_SRC_COMMANDS_PROP_VALUES_H_ +#define LIBWEAVE_SRC_COMMANDS_PROP_VALUES_H_ + +#include <map> +#include <memory> +#include <string> + +#include <weave/error.h> + +#include "src/commands/schema_utils.h" + +namespace base { +class Value; +class DictionaryValue; +} // namespace base + +namespace weave { + +// Enumeration to indicate supported command parameter types. +enum class ValueType { + Int, + Double, + String, + Boolean, + Object, + Array, +}; + +class PropValue; +class IntValue; +class DoubleValue; +class StringValue; +class BooleanValue; +class ObjectValue; +class ArrayValue; + +class PropType; + +// Helper methods to get the parameter type enum value for the given +// native C++ data representation. +// The generic GetValueType<T>() is undefined, however particular +// type specializations return the appropriate ValueType. +template <typename T> +ValueType GetValueType(); // Undefined. +template <> +inline ValueType GetValueType<int>() { + return ValueType::Int; +} +template <> +inline ValueType GetValueType<double>() { + return ValueType::Double; +} +template <> +inline ValueType GetValueType<std::string>() { + return ValueType::String; +} +template <> +inline ValueType GetValueType<bool>() { + return ValueType::Boolean; +} +template <> +inline ValueType GetValueType<ValueMap>() { + return ValueType::Object; +} +template <> +inline ValueType GetValueType<ValueVector>() { + return ValueType::Array; +} + +// The base class for property values. +// Concrete value classes of various types will be derived from this base. +// A property value is the actual command parameter value (or a concrete value +// that can be used in constraints and presets). The PropValue is mostly +// just parsed content of base::Value when a command is dispatched, however +// it does have some additional functionality: +// - it has a reference to the type definition (PropType) which is used +// when validating the value, especially for "object" types. +// - it can be compared with another instances of values of the same type. +// This is used to validate the values against "enum"/"one of" constraints. +class PropValue { + public: + // Only CreateDefaultValue should use this constructor. + explicit PropValue(const PropType& type); + virtual ~PropValue(); + + // Gets the type of the value. + virtual ValueType GetType() const = 0; + + // Type conversion methods. Used in lieu of RTTI and dynamic_cast<>. + virtual IntValue* GetInt() { return nullptr; } + virtual IntValue const* GetInt() const { return nullptr; } + virtual DoubleValue* GetDouble() { return nullptr; } + virtual DoubleValue const* GetDouble() const { return nullptr; } + virtual StringValue* GetString() { return nullptr; } + virtual StringValue const* GetString() const { return nullptr; } + virtual BooleanValue* GetBoolean() { return nullptr; } + virtual BooleanValue const* GetBoolean() const { return nullptr; } + virtual ObjectValue* GetObject() { return nullptr; } + virtual ObjectValue const* GetObject() const { return nullptr; } + virtual ArrayValue* GetArray() { return nullptr; } + virtual ArrayValue const* GetArray() const { return nullptr; } + + // Makes a full copy of this value class. + virtual std::unique_ptr<PropValue> Clone() const = 0; + + // Saves the value as a JSON object. Never fails. + virtual std::unique_ptr<base::Value> ToJson() const = 0; + + // Return the type definition of this value. + const PropType* GetPropType() const { return type_.get(); } + // Compares two values and returns true if they are equal. + virtual bool IsEqual(const PropValue* value) const = 0; + + protected: + // Special out-of-line constructor to help implement PropValue::Clone(). + // That method needs to clone the underlying type but can't do this in this + // header file since PropType is just forward-declared (it needs PropValue + // fully defined in its own inner workings). + explicit PropValue(const PropValue& other); + + private: + std::unique_ptr<const PropType> type_; +}; + +// A helper template base class for implementing value classes. +template <typename T> +class TypedValueBase : public PropValue { + public: + using PropValue::PropValue; + + // Overrides from PropValue base class. + ValueType GetType() const override { return GetValueType<T>(); } + + std::unique_ptr<base::Value> ToJson() const override { + return TypedValueToJson(value_); + } + + bool IsEqual(const PropValue* value) const override { + if (GetType() != value->GetType()) + return false; + return CompareValue(GetValue(), + static_cast<const TypedValueBase*>(value)->GetValue()); + } + + // Helper methods to get and set the C++ representation of the value. + const T& GetValue() const { return value_; } + + protected: + explicit TypedValueBase(const TypedValueBase& other) + : PropValue(other), value_(other.value_) {} + + TypedValueBase(const PropType& type, T value) + : PropValue(type), value_(value) {} + + private: + T value_{}; // The value of the parameter in C++ data representation. +}; + +// A helper template base class for implementing value classes. +template <typename Derived, typename T> +class TypedValueWithClone : public TypedValueBase<T> { + public: + using Base = TypedValueWithClone<Derived, T>; + + // Expose the custom constructor of the base class. + using TypedValueBase<T>::TypedValueBase; + using PropValue::GetPropType; + + std::unique_ptr<PropValue> Clone() const override { + return std::unique_ptr<PropValue>{ + new Derived{*static_cast<const Derived*>(this)}}; + } + + static std::unique_ptr<Derived> CreateFromJson(const base::Value& value, + const PropType& type, + ErrorPtr* error) { + T tmp_value; + if (!TypedValueFromJson(&value, &type, &tmp_value, error)) + return nullptr; + + // Only place where invalid value can exist. + std::unique_ptr<Derived> result{new Derived{type, tmp_value}}; + if (!result->GetPropType()->ValidateConstraints(*result, error)) + return nullptr; + + return result; + } +}; + +// Value of type Integer. +class IntValue final : public TypedValueWithClone<IntValue, int> { + public: + using Base::Base; + friend class TypedValueWithClone<IntValue, int>; + IntValue* GetInt() override { return this; } + IntValue const* GetInt() const override { return this; } +}; + +// Value of type Number. +class DoubleValue final : public TypedValueWithClone<DoubleValue, double> { + public: + using Base::Base; + friend class TypedValueWithClone<DoubleValue, double>; + DoubleValue* GetDouble() override { return this; } + DoubleValue const* GetDouble() const override { return this; } +}; + +// Value of type String. +class StringValue final : public TypedValueWithClone<StringValue, std::string> { + public: + using Base::Base; + friend class TypedValueWithClone<StringValue, std::string>; + StringValue* GetString() override { return this; } + StringValue const* GetString() const override { return this; } +}; + +// Value of type Boolean. +class BooleanValue final : public TypedValueWithClone<BooleanValue, bool> { + public: + using Base::Base; + friend class TypedValueWithClone<BooleanValue, bool>; + BooleanValue* GetBoolean() override { return this; } + BooleanValue const* GetBoolean() const override { return this; } +}; + +// Value of type Object. +class ObjectValue final : public TypedValueWithClone<ObjectValue, ValueMap> { + public: + using Base::Base; + friend class TypedValueWithClone<ObjectValue, ValueMap>; + ObjectValue* GetObject() override { return this; } + ObjectValue const* GetObject() const override { return this; } +}; + +// Value of type Array. +class ArrayValue final : public TypedValueWithClone<ArrayValue, ValueVector> { + public: + using Base::Base; + friend class TypedValueWithClone<ArrayValue, ValueVector>; + ArrayValue* GetArray() override { return this; } + ArrayValue const* GetArray() const override { return this; } +}; + +} // namespace weave + +#endif // LIBWEAVE_SRC_COMMANDS_PROP_VALUES_H_
diff --git a/src/commands/schema_constants.cc b/src/commands/schema_constants.cc index cd8bf84..c99536b 100644 --- a/src/commands/schema_constants.cc +++ b/src/commands/schema_constants.cc
@@ -10,11 +10,20 @@ namespace commands { const char kDomain[] = "command_schema"; +const char kOutOfRange[] = "out_of_range"; const char kTypeMismatch[] = "type_mismatch"; +const char kPropTypeChanged[] = "param_type_changed"; +const char kUnknownType[] = "unknown_type"; +const char kInvalidPropDef[] = "invalid_parameter_definition"; const char kInvalidPropValue[] = "invalid_parameter_value"; +const char kNoTypeInfo[] = "no_type_info"; const char kPropertyMissing[] = "parameter_missing"; +const char kUnknownProperty[] = "unexpected_parameter"; +const char kInvalidObjectSchema[] = "invalid_object_schema"; +const char kDuplicateCommandDef[] = "duplicate_command_definition"; const char kInvalidCommandName[] = "invalid_command_name"; const char kCommandFailed[] = "command_failed"; +const char kInvalidCommandVisibility[] = "invalid_command_visibility"; const char kInvalidMinimalRole[] = "invalid_minimal_role"; const char kCommandDestroyed[] = "command_destroyed"; const char kInvalidState[] = "invalid_state"; @@ -24,6 +33,25 @@ namespace commands { namespace attributes { +const char kType[] = "type"; +const char kDisplayName[] = "displayName"; +const char kDefault[] = "default"; +const char kItems[] = "items"; +const char kIsRequired[] = "isRequired"; + +const char kNumeric_Min[] = "minimum"; +const char kNumeric_Max[] = "maximum"; + +const char kString_MinLength[] = "minLength"; +const char kString_MaxLength[] = "maxLength"; + +const char kOneOf_Enum[] = "enum"; +const char kOneOf_Metadata[] = "metadata"; + +const char kObject_Properties[] = "properties"; +const char kObject_AdditionalProperties[] = "additionalProperties"; +const char kObject_Required[] = "required"; + const char kCommand_Id[] = "id"; const char kCommand_Name[] = "name"; const char kCommand_Parameters[] = "parameters"; @@ -38,6 +66,12 @@ const char kCommand_Role_User[] = "user"; const char kCommand_Role_Viewer[] = "viewer"; +const char kCommand_Visibility[] = "visibility"; +const char kCommand_Visibility_None[] = "none"; +const char kCommand_Visibility_Local[] = "local"; +const char kCommand_Visibility_Cloud[] = "cloud"; +const char kCommand_Visibility_All[] = "all"; + } // namespace attributes } // namespace commands
diff --git a/src/commands/schema_constants.h b/src/commands/schema_constants.h index 57766e6..742245f 100644 --- a/src/commands/schema_constants.h +++ b/src/commands/schema_constants.h
@@ -13,11 +13,20 @@ extern const char kDomain[]; // Common command definition error codes. +extern const char kOutOfRange[]; extern const char kTypeMismatch[]; +extern const char kPropTypeChanged[]; +extern const char kUnknownType[]; +extern const char kInvalidPropDef[]; extern const char kInvalidPropValue[]; +extern const char kNoTypeInfo[]; extern const char kPropertyMissing[]; +extern const char kUnknownProperty[]; +extern const char kInvalidObjectSchema[]; +extern const char kDuplicateCommandDef[]; extern const char kInvalidCommandName[]; extern const char kCommandFailed[]; +extern const char kInvalidCommandVisibility[]; extern const char kInvalidMinimalRole[]; extern const char kCommandDestroyed[]; extern const char kInvalidState[]; @@ -27,6 +36,25 @@ namespace commands { namespace attributes { // Command description JSON schema attributes. +extern const char kType[]; +extern const char kDisplayName[]; +extern const char kDefault[]; +extern const char kItems[]; +extern const char kIsRequired[]; + +extern const char kNumeric_Min[]; +extern const char kNumeric_Max[]; + +extern const char kString_MinLength[]; +extern const char kString_MaxLength[]; + +extern const char kOneOf_Enum[]; +extern const char kOneOf_Metadata[]; + +extern const char kObject_Properties[]; +extern const char kObject_AdditionalProperties[]; +extern const char kObject_Required[]; + extern const char kCommand_Id[]; extern const char kCommand_Name[]; extern const char kCommand_Parameters[]; @@ -41,6 +69,12 @@ extern const char kCommand_Role_User[]; extern const char kCommand_Role_Viewer[]; +extern const char kCommand_Visibility[]; +extern const char kCommand_Visibility_None[]; +extern const char kCommand_Visibility_Local[]; +extern const char kCommand_Visibility_Cloud[]; +extern const char kCommand_Visibility_All[]; + } // namespace attributes } // namespace commands
diff --git a/src/commands/schema_utils.cc b/src/commands/schema_utils.cc new file mode 100644 index 0000000..3c3e949 --- /dev/null +++ b/src/commands/schema_utils.cc
@@ -0,0 +1,266 @@ +// 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/schema_utils.h" + +#include <algorithm> +#include <set> +#include <string> + +#include <base/json/json_writer.h> +#include <base/logging.h> + +#include "src/commands/object_schema.h" +#include "src/commands/prop_types.h" +#include "src/commands/prop_values.h" + +namespace weave { +namespace { +// Helper function to report "type mismatch" errors when parsing JSON. +void ReportJsonTypeMismatch(const tracked_objects::Location& location, + const base::Value* value_in, + const std::string& expected_type, + ErrorPtr* error) { + std::string value_as_string; + base::JSONWriter::Write(*value_in, &value_as_string); + Error::AddToPrintf(error, location, errors::commands::kDomain, + errors::commands::kTypeMismatch, + "Unable to convert value %s into %s", + value_as_string.c_str(), expected_type.c_str()); +} + +// Template version of ReportJsonTypeMismatch that deduces the type of expected +// data from the value_out parameter passed to particular overload of +// TypedValueFromJson() function. Always returns false. +template <typename T> +bool ReportUnexpectedJson(const tracked_objects::Location& location, + const base::Value* value_in, + T*, + ErrorPtr* error) { + ReportJsonTypeMismatch(location, value_in, + PropType::GetTypeStringFromType(GetValueType<T>()), + error); + return false; +} + +bool ErrorMissingProperty(ErrorPtr* error, + const tracked_objects::Location& location, + const char* param_name) { + Error::AddToPrintf(error, location, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "Required parameter missing: %s", param_name); + return false; +} + +} // namespace + +// Specializations of TypedValueToJson<T>() for supported C++ types. +std::unique_ptr<base::FundamentalValue> TypedValueToJson(bool value) { + return std::unique_ptr<base::FundamentalValue>( + new base::FundamentalValue(value)); +} + +std::unique_ptr<base::FundamentalValue> TypedValueToJson(int value) { + return std::unique_ptr<base::FundamentalValue>( + new base::FundamentalValue(value)); +} + +std::unique_ptr<base::FundamentalValue> TypedValueToJson(double value) { + return std::unique_ptr<base::FundamentalValue>( + new base::FundamentalValue(value)); +} + +std::unique_ptr<base::StringValue> TypedValueToJson(const std::string& value) { + return std::unique_ptr<base::StringValue>(new base::StringValue(value)); +} + +std::unique_ptr<base::DictionaryValue> TypedValueToJson(const ValueMap& value) { + std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + for (const auto& pair : value) { + auto prop_value = pair.second->ToJson(); + CHECK(prop_value); + dict->SetWithoutPathExpansion(pair.first, prop_value.release()); + } + return dict; +} + +std::unique_ptr<base::ListValue> TypedValueToJson(const ValueVector& value) { + std::unique_ptr<base::ListValue> list(new base::ListValue); + for (const auto& item : value) { + auto json = item->ToJson(); + CHECK(json); + list->Append(json.release()); + } + return list; +} + +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + bool* value_out, + ErrorPtr* error) { + return value_in->GetAsBoolean(value_out) || + ReportUnexpectedJson(FROM_HERE, value_in, value_out, error); +} + +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + int* value_out, + ErrorPtr* error) { + return value_in->GetAsInteger(value_out) || + ReportUnexpectedJson(FROM_HERE, value_in, value_out, error); +} + +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + double* value_out, + ErrorPtr* error) { + return value_in->GetAsDouble(value_out) || + ReportUnexpectedJson(FROM_HERE, value_in, value_out, error); +} + +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + std::string* value_out, + ErrorPtr* error) { + return value_in->GetAsString(value_out) || + ReportUnexpectedJson(FROM_HERE, value_in, value_out, error); +} + +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + ValueMap* value_out, + ErrorPtr* error) { + const base::DictionaryValue* dict = nullptr; + if (!value_in->GetAsDictionary(&dict)) + return ReportUnexpectedJson(FROM_HERE, value_in, value_out, error); + + CHECK(type) << "Object definition must be provided"; + CHECK(ValueType::Object == type->GetType()) << "Type must be Object"; + + const ObjectSchema* object_schema = type->GetObject()->GetObjectSchemaPtr(); + std::set<std::string> keys_processed; + value_out->clear(); // Clear possible default values already in |value_out|. + for (const auto& pair : object_schema->GetProps()) { + const PropValue* def_value = pair.second->GetDefaultValue(); + if (dict->HasKey(pair.first)) { + const base::Value* param_value = nullptr; + CHECK(dict->GetWithoutPathExpansion(pair.first, ¶m_value)) + << "Unable to get parameter"; + auto value = pair.second->CreatePropValue(*param_value, error); + if (!value) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidPropValue, + "Invalid value for property '%s'", + pair.first.c_str()); + return false; + } + value_out->emplace_hint(value_out->end(), pair.first, std::move(value)); + keys_processed.insert(pair.first); + } else if (def_value) { + value_out->emplace_hint(value_out->end(), pair.first, def_value->Clone()); + keys_processed.insert(pair.first); + } else if (pair.second->IsRequired()) { + return ErrorMissingProperty(error, FROM_HERE, pair.first.c_str()); + } + } + + // Just for sanity, make sure that we processed all the necessary properties + // and there weren't any extra (unknown) ones specified. If so, ignore + // them, but log as warnings... + base::DictionaryValue::Iterator iter(*dict); + while (!iter.IsAtEnd()) { + std::string key = iter.key(); + if (keys_processed.find(key) == keys_processed.end() && + !object_schema->GetExtraPropertiesAllowed()) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kUnknownProperty, + "Unrecognized parameter '%s'", key.c_str()); + return false; + } + iter.Advance(); + } + + // Now go over all property values and validate them. + for (const auto& pair : *value_out) { + const PropType* prop_type = pair.second->GetPropType(); + CHECK(prop_type) << "Value property type must be available"; + if (!prop_type->ValidateConstraints(*pair.second, error)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidPropValue, + "Invalid value for property '%s'", pair.first.c_str()); + return false; + } + } + return true; +} + +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + ValueVector* value_out, + ErrorPtr* error) { + const base::ListValue* list = nullptr; + if (!value_in->GetAsList(&list)) + return ReportUnexpectedJson(FROM_HERE, value_in, value_out, error); + + CHECK(type) << "Array type definition must be provided"; + CHECK(ValueType::Array == type->GetType()) << "Type must be Array"; + const PropType* item_type = type->GetArray()->GetItemTypePtr(); + CHECK(item_type) << "Incomplete array type definition"; + + // This value might already contain values from the type defaults. + // Clear them first before proceeding. + value_out->clear(); + value_out->reserve(list->GetSize()); + for (const base::Value* item : *list) { + std::unique_ptr<PropValue> prop_value = + item_type->CreatePropValue(*item, error); + if (!prop_value) + return false; + value_out->push_back(std::move(prop_value)); + } + return true; +} + +// Compares two sets of key-value pairs from two Objects. +static bool obj_cmp(const ValueMap::value_type& v1, + const ValueMap::value_type& v2) { + return (v1.first == v2.first) && v1.second->IsEqual(v2.second.get()); +} + +bool operator==(const ValueMap& obj1, const ValueMap& obj2) { + if (obj1.size() != obj2.size()) + return false; + + auto pair = std::mismatch(obj1.begin(), obj1.end(), obj2.begin(), obj_cmp); + return pair == std::make_pair(obj1.end(), obj2.end()); +} + +bool operator==(const ValueVector& arr1, const ValueVector& arr2) { + if (arr1.size() != arr2.size()) + return false; + + using Type = const ValueVector::value_type; + // Compare two array items. + auto arr_cmp = [](const Type& v1, const Type& v2) { + return v1->IsEqual(v2.get()); + }; + auto pair = std::mismatch(arr1.begin(), arr1.end(), arr2.begin(), arr_cmp); + return pair == std::make_pair(arr1.end(), arr2.end()); +} + +std::string ToString(const ValueMap& obj) { + auto val = TypedValueToJson(obj); + std::string str; + base::JSONWriter::Write(*val, &str); + return str; +} + +std::string ToString(const ValueVector& arr) { + auto val = TypedValueToJson(arr); + std::string str; + base::JSONWriter::Write(*val, &str); + return str; +} + +} // namespace weave
diff --git a/src/commands/schema_utils.h b/src/commands/schema_utils.h new file mode 100644 index 0000000..0c1d1b3 --- /dev/null +++ b/src/commands/schema_utils.h
@@ -0,0 +1,136 @@ +// 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. + +#ifndef LIBWEAVE_SRC_COMMANDS_SCHEMA_UTILS_H_ +#define LIBWEAVE_SRC_COMMANDS_SCHEMA_UTILS_H_ + +#include <cmath> +#include <limits> +#include <map> +#include <memory> +#include <string> +#include <type_traits> +#include <vector> + +#include <base/logging.h> +#include <base/values.h> +#include <weave/error.h> + +namespace weave { + +class PropType; +class PropValue; +class ObjectSchema; +class ObjectValue; + +// C++ representation of object values. +using ValueMap = std::map<std::string, std::shared_ptr<const PropValue>>; + +// C++ representation of array of values. +using ValueVector = std::vector<std::shared_ptr<const PropValue>>; + +// Converts an object to string. +std::string ToString(const ValueMap& obj); + +// Converts an array to string. +std::string ToString(const ValueVector& arr); + +// InheritableAttribute class is used for specifying various command parameter +// attributes that can be inherited from a base (parent) schema. +// The |value| still specifies the actual attribute values, whether it +// is inherited or overridden, while |is_inherited| can be used to identify +// if the attribute was inherited (true) or overridden (false). +template <typename T> +class InheritableAttribute final { + public: + InheritableAttribute() = default; + explicit InheritableAttribute(T val) + : value(std::move(val)), is_inherited(true) {} + InheritableAttribute(T val, bool inherited) + : value(std::move(val)), is_inherited(inherited) {} + T value{}; + bool is_inherited{true}; +}; + +// A bunch of helper function to create base::Value for specific C++ classes, +// including vectors of types. These are used in template classes below +// to simplify specialization logic. +std::unique_ptr<base::FundamentalValue> TypedValueToJson(bool value); +std::unique_ptr<base::FundamentalValue> TypedValueToJson(int value); +std::unique_ptr<base::FundamentalValue> TypedValueToJson(double value); +std::unique_ptr<base::StringValue> TypedValueToJson(const std::string& value); +std::unique_ptr<base::DictionaryValue> TypedValueToJson(const ValueMap& value); +std::unique_ptr<base::ListValue> TypedValueToJson(const ValueVector& value); +template <typename T> +std::unique_ptr<base::ListValue> TypedValueToJson( + const std::vector<T>& values) { + std::unique_ptr<base::ListValue> list(new base::ListValue); + for (const auto& v : values) { + auto json = TypedValueToJson(v); + CHECK(json); + list->Append(json.release()); + } + return list; +} + +// Similarly to TypedValueToJson() function above, the following overloaded +// helper methods allow to extract specific C++ data types from base::Value. +// Also used in template classes below to simplify specialization logic. +// TODO(vitalybuka): Fix this. Interface is misleading. Seeing PropType internal +// type validation is expected. In reality only ValueMap and ValueVector do +// validation. +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + bool* value_out, + ErrorPtr* error); +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + int* value_out, + ErrorPtr* error); +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + double* value_out, + ErrorPtr* error); +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + std::string* value_out, + ErrorPtr* error); +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + ValueMap* value_out, + ErrorPtr* error); +bool TypedValueFromJson(const base::Value* value_in, + const PropType* type, + ValueVector* value_out, + ErrorPtr* error); + +bool operator==(const ValueMap& obj1, const ValueMap& obj2); +bool operator==(const ValueVector& arr1, const ValueVector& arr2); + +// CompareValue is a helper function to help with implementing EqualsTo operator +// for various data types. For most scalar types it is using operator==(), +// however, for floating point values, rounding errors in binary representation +// of IEEE floats/doubles can cause straight == comparison to fail for seemingly +// equivalent values. For these, use approximate comparison with the error +// margin equal to the epsilon value defined for the corresponding data type. +// This is used when looking up values for implementation of OneOf constraints +// which should work reliably for floating points also ("number" type). + +// Compare exact types using ==. +template <typename T> +inline typename std::enable_if<!std::is_floating_point<T>::value, bool>::type +CompareValue(const T& v1, const T& v2) { + return v1 == v2; +} + +// Compare non-exact types (such as double) using precision margin (epsilon). +template <typename T> +inline typename std::enable_if<std::is_floating_point<T>::value, bool>::type +CompareValue(const T& v1, const T& v2) { + return std::abs(v1 - v2) <= std::numeric_limits<T>::epsilon(); +} + +} // namespace weave + +#endif // LIBWEAVE_SRC_COMMANDS_SCHEMA_UTILS_H_
diff --git a/src/commands/schema_utils_unittest.cc b/src/commands/schema_utils_unittest.cc new file mode 100644 index 0000000..1ea4c90 --- /dev/null +++ b/src/commands/schema_utils_unittest.cc
@@ -0,0 +1,212 @@ +// 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/schema_utils.h" + +#include <memory> +#include <string> +#include <vector> + +#include <base/values.h> +#include <gtest/gtest.h> + +#include "src/commands/object_schema.h" +#include "src/commands/prop_types.h" +#include "src/commands/prop_values.h" +#include "src/commands/schema_constants.h" +#include "src/commands/unittest_utils.h" + +namespace weave { + +using test::CreateDictionaryValue; +using test::CreateValue; + +TEST(CommandSchemaUtils, TypedValueToJson_Scalar) { + EXPECT_JSON_EQ("true", *TypedValueToJson(true)); + EXPECT_JSON_EQ("false", *TypedValueToJson(false)); + + EXPECT_JSON_EQ("0", *TypedValueToJson(0)); + EXPECT_JSON_EQ("-10", *TypedValueToJson(-10)); + EXPECT_JSON_EQ("20", *TypedValueToJson(20)); + + EXPECT_JSON_EQ("0.0", *TypedValueToJson(0.0)); + EXPECT_JSON_EQ("1.2", *TypedValueToJson(1.2)); + + EXPECT_JSON_EQ("'abc'", *TypedValueToJson(std::string("abc"))); + + std::vector<bool> bool_array{true, false}; + EXPECT_JSON_EQ("[true,false]", *TypedValueToJson(bool_array)); + + std::vector<int> int_array{1, 2, 5}; + EXPECT_JSON_EQ("[1,2,5]", *TypedValueToJson(int_array)); + + std::vector<double> dbl_array{1.1, 2.2}; + EXPECT_JSON_EQ("[1.1,2.2]", *TypedValueToJson(dbl_array)); + + std::vector<std::string> str_array{"a", "bc"}; + EXPECT_JSON_EQ("['a','bc']", *TypedValueToJson(str_array)); +} + +TEST(CommandSchemaUtils, TypedValueToJson_Object) { + IntPropType int_type; + ValueMap object; + + object.insert(std::make_pair( + "width", int_type.CreateValue(base::FundamentalValue{640}, nullptr))); + object.insert(std::make_pair( + "height", int_type.CreateValue(base::FundamentalValue{480}, nullptr))); + EXPECT_JSON_EQ("{'height':480,'width':640}", *TypedValueToJson(object)); +} + +TEST(CommandSchemaUtils, TypedValueToJson_Array) { + IntPropType int_type; + ValueVector arr; + + arr.push_back(int_type.CreateValue(base::FundamentalValue{640}, nullptr)); + arr.push_back(int_type.CreateValue(base::FundamentalValue{480}, nullptr)); + EXPECT_JSON_EQ("[640,480]", *TypedValueToJson(arr)); +} + +TEST(CommandSchemaUtils, TypedValueFromJson_Bool) { + bool value; + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("true").get(), nullptr, &value, nullptr)); + EXPECT_TRUE(value); + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("false").get(), nullptr, &value, nullptr)); + EXPECT_FALSE(value); + + ErrorPtr error; + EXPECT_FALSE( + TypedValueFromJson(CreateValue("0").get(), nullptr, &value, &error)); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); + error.reset(); +} + +TEST(CommandSchemaUtils, TypedValueFromJson_Int) { + int value; + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("0").get(), nullptr, &value, nullptr)); + EXPECT_EQ(0, value); + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("23").get(), nullptr, &value, nullptr)); + EXPECT_EQ(23, value); + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("-1234").get(), nullptr, &value, nullptr)); + EXPECT_EQ(-1234, value); + + ErrorPtr error; + EXPECT_FALSE( + TypedValueFromJson(CreateValue("'abc'").get(), nullptr, &value, &error)); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); + error.reset(); +} + +TEST(CommandSchemaUtils, TypedValueFromJson_Double) { + double value; + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("0").get(), nullptr, &value, nullptr)); + EXPECT_DOUBLE_EQ(0.0, value); + EXPECT_TRUE( + TypedValueFromJson(CreateValue("0.0").get(), nullptr, &value, nullptr)); + EXPECT_DOUBLE_EQ(0.0, value); + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("23").get(), nullptr, &value, nullptr)); + EXPECT_EQ(23.0, value); + EXPECT_TRUE( + TypedValueFromJson(CreateValue("23.1").get(), nullptr, &value, nullptr)); + EXPECT_EQ(23.1, value); + + EXPECT_TRUE(TypedValueFromJson(CreateValue("-1.23E+02").get(), nullptr, + &value, nullptr)); + EXPECT_EQ(-123.0, value); + + ErrorPtr error; + EXPECT_FALSE( + TypedValueFromJson(CreateValue("'abc'").get(), nullptr, &value, &error)); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); + error.reset(); +} + +TEST(CommandSchemaUtils, TypedValueFromJson_String) { + std::string value; + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("''").get(), nullptr, &value, nullptr)); + EXPECT_EQ("", value); + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("'23'").get(), nullptr, &value, nullptr)); + EXPECT_EQ("23", value); + + EXPECT_TRUE( + TypedValueFromJson(CreateValue("'abc'").get(), nullptr, &value, nullptr)); + EXPECT_EQ("abc", value); + + ErrorPtr error; + EXPECT_FALSE( + TypedValueFromJson(CreateValue("12").get(), nullptr, &value, &error)); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); + error.reset(); +} + +TEST(CommandSchemaUtils, TypedValueFromJson_Object) { + ValueMap value; + std::unique_ptr<ObjectSchema> schema{new ObjectSchema}; + + IntPropType age_prop; + age_prop.AddMinMaxConstraint(0, 150); + schema->AddProp("age", age_prop.Clone()); + + StringPropType name_prop; + name_prop.AddLengthConstraint(1, 30); + schema->AddProp("name", name_prop.Clone()); + + ObjectPropType type; + type.SetObjectSchema(std::move(schema)); + EXPECT_TRUE(TypedValueFromJson(CreateValue("{'age':20,'name':'Bob'}").get(), + &type, &value, nullptr)); + ValueMap value2; + value2.insert(std::make_pair( + "age", age_prop.CreateValue(base::FundamentalValue{20}, nullptr))); + value2.insert(std::make_pair( + "name", name_prop.CreateValue(base::StringValue("Bob"), nullptr))); + EXPECT_EQ(value2, value); + + ErrorPtr error; + EXPECT_FALSE( + TypedValueFromJson(CreateValue("'abc'").get(), nullptr, &value, &error)); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); + error.reset(); +} + +TEST(CommandSchemaUtils, TypedValueFromJson_Array) { + ValueVector arr; + StringPropType str_type; + str_type.AddLengthConstraint(3, 100); + ArrayPropType type; + type.SetItemType(str_type.Clone()); + + EXPECT_TRUE(TypedValueFromJson(CreateValue("['foo', 'bar']").get(), &type, + &arr, nullptr)); + ValueVector arr2; + arr2.push_back(str_type.CreateValue(base::StringValue{"foo"}, nullptr)); + arr2.push_back(str_type.CreateValue(base::StringValue{"bar"}, nullptr)); + EXPECT_EQ(arr2, arr); + + ErrorPtr error; + EXPECT_FALSE(TypedValueFromJson(CreateValue("['baz', 'ab']").get(), &type, + &arr, &error)); + EXPECT_EQ(errors::commands::kOutOfRange, error->GetCode()); + error.reset(); +} + +} // namespace weave
diff --git a/src/commands/unittest_utils.h b/src/commands/unittest_utils.h new file mode 100644 index 0000000..25392ee --- /dev/null +++ b/src/commands/unittest_utils.h
@@ -0,0 +1,47 @@ +// 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. + +#ifndef LIBWEAVE_SRC_COMMANDS_UNITTEST_UTILS_H_ +#define LIBWEAVE_SRC_COMMANDS_UNITTEST_UTILS_H_ + +#include <memory> +#include <string> + +#include <base/values.h> +#include <gtest/gtest.h> +#include <weave/test/unittest_utils.h> + +#include "src/commands/prop_types.h" +#include "src/commands/prop_values.h" + +namespace weave { +namespace test { + +template <typename T> +std::unique_ptr<const PropValue> make_prop_value(const base::Value& value) { + auto prop_type = PropType::Create(GetValueType<T>()); + return prop_type->CreatePropValue(value, nullptr); +} + +inline std::unique_ptr<const PropValue> make_int_prop_value(int value) { + return make_prop_value<int>(base::FundamentalValue{value}); +} + +inline std::unique_ptr<const PropValue> make_double_prop_value(double value) { + return make_prop_value<double>(base::FundamentalValue{value}); +} + +inline std::unique_ptr<const PropValue> make_bool_prop_value(bool value) { + return make_prop_value<bool>(base::FundamentalValue{value}); +} + +inline std::unique_ptr<const PropValue> make_string_prop_value( + const std::string& value) { + return make_prop_value<std::string>(base::StringValue{value}); +} + +} // namespace test +} // namespace weave + +#endif // LIBWEAVE_SRC_COMMANDS_UNITTEST_UTILS_H_
diff --git a/src/config.cc b/src/config.cc index fe50e3b..52f5ea1 100644 --- a/src/config.cc +++ b/src/config.cc
@@ -65,7 +65,7 @@ result.oauth_url = "https://accounts.google.com/o/oauth2/"; result.service_url = kWeaveUrl; result.local_anonymous_access_role = AuthScope::kViewer; - result.pairing_modes.insert(PairingType::kPinCode); + result.pairing_modes.emplace(PairingType::kPinCode); result.device_id = base::GenerateGUID(); return result; }
diff --git a/src/config_unittest.cc b/src/config_unittest.cc index c36bb25..10ed07e 100644 --- a/src/config_unittest.cc +++ b/src/config_unittest.cc
@@ -10,7 +10,8 @@ #include <gmock/gmock.h> #include <gtest/gtest.h> #include <weave/provider/test/mock_config_store.h> -#include <weave/test/unittest_utils.h> + +#include "src/commands/unittest_utils.h" using testing::_; using testing::Invoke;
diff --git a/src/data_encoding_unittest.cc b/src/data_encoding_unittest.cc index 53dc534..7e3bfe4 100644 --- a/src/data_encoding_unittest.cc +++ b/src/data_encoding_unittest.cc
@@ -30,7 +30,7 @@ EXPECT_EQ("q=test&path=%2Fusr%2Fbin&%23=%25", encoded); auto params = WebParamsDecode(encoded); - EXPECT_EQ(3u, params.size()); + EXPECT_EQ(3, params.size()); EXPECT_EQ("q", params[0].first); EXPECT_EQ("test", params[0].second); EXPECT_EQ("path", params[1].first);
diff --git a/src/device_manager.cc b/src/device_manager.cc index 52b2882..c0b8e9a 100644 --- a/src/device_manager.cc +++ b/src/device_manager.cc
@@ -134,7 +134,7 @@ return state_manager_->SetProperties(dict, error); } -const base::Value* DeviceManager::GetStateProperty( +std::unique_ptr<base::Value> DeviceManager::GetStateProperty( const std::string& name) const { return state_manager_->GetProperty(name); } @@ -145,7 +145,7 @@ return state_manager_->SetProperty(name, value, error); } -const base::DictionaryValue& DeviceManager::GetState() const { +std::unique_ptr<base::DictionaryValue> DeviceManager::GetState() const { return state_manager_->GetState(); }
diff --git a/src/device_manager.h b/src/device_manager.h index 6c3df05..ccf8778 100644 --- a/src/device_manager.h +++ b/src/device_manager.h
@@ -52,11 +52,12 @@ ErrorPtr* error) override; bool SetStateProperties(const base::DictionaryValue& dict, ErrorPtr* error) override; - const base::Value* GetStateProperty(const std::string& name) const override; + std::unique_ptr<base::Value> GetStateProperty( + const std::string& name) const override; bool SetStateProperty(const std::string& name, const base::Value& value, ErrorPtr* error) override; - const base::DictionaryValue& GetState() const override; + std::unique_ptr<base::DictionaryValue> GetState() const override; void Register(const std::string& ticket_id, const DoneCallback& callback) override; GcdState GetGcdState() const override;
diff --git a/src/device_registration_info.cc b/src/device_registration_info.cc index 0c914b7..751e530 100644 --- a/src/device_registration_info.cc +++ b/src/device_registration_info.cc
@@ -480,12 +480,14 @@ std::unique_ptr<base::DictionaryValue> DeviceRegistrationInfo::BuildDeviceResource(ErrorPtr* error) { // Limit only to commands that are visible to the cloud. - auto commands = - command_manager_->GetCommandDictionary().GetCommandsAsJson(error); + auto commands = command_manager_->GetCommandDictionary().GetCommandsAsJson( + [](const CommandDefinition* def) { return def->GetVisibility().cloud; }, + true, error); if (!commands) return nullptr; - const base::DictionaryValue& state = state_manager_->GetState(); + std::unique_ptr<base::DictionaryValue> state = state_manager_->GetState(); + CHECK(state); std::unique_ptr<base::DictionaryValue> resource{new base::DictionaryValue}; if (!GetSettings().cloud_id.empty()) @@ -506,7 +508,7 @@ } resource->Set("channel", channel.release()); resource->Set("commandDefs", commands.release()); - resource->Set("state", state.DeepCopy()); + resource->Set("state", state.release()); return resource; } @@ -1119,11 +1121,23 @@ return; std::unique_ptr<base::ListValue> patches{new base::ListValue}; - for (auto& state_change : state_changes) { + for (const auto& state_change : state_changes) { std::unique_ptr<base::DictionaryValue> patch{new base::DictionaryValue}; patch->SetString("timeMs", std::to_string(state_change.timestamp.ToJavaTime())); - patch->Set("patch", state_change.changed_properties.release()); + + std::unique_ptr<base::DictionaryValue> changes{new base::DictionaryValue}; + for (const auto& pair : state_change.changed_properties) { + auto value = pair.second->ToJson(); + CHECK(value); + // The key in |pair.first| is the full property name in format + // "package.property_name", so must use DictionaryValue::Set() instead of + // DictionaryValue::SetWithoutPathExpansion to recreate the JSON + // property tree properly. + changes->Set(pair.first, value.release()); + } + patch->Set("patch", changes.release()); + patches->Append(patch.release()); }
diff --git a/src/device_registration_info_unittest.cc b/src/device_registration_info_unittest.cc index f2fe4c4..df4a438 100644 --- a/src/device_registration_info_unittest.cc +++ b/src/device_registration_info_unittest.cc
@@ -11,10 +11,10 @@ #include <weave/provider/test/fake_task_runner.h> #include <weave/provider/test/mock_config_store.h> #include <weave/provider/test/mock_http_client.h> -#include <weave/test/unittest_utils.h> #include "src/bind_lambda.h" #include "src/commands/command_manager.h" +#include "src/commands/unittest_utils.h" #include "src/http_constants.h" #include "src/states/mock_state_change_queue_interface.h" #include "src/states/state_manager.h" @@ -355,14 +355,16 @@ auto json_cmds = CreateDictionaryValue(R"({ 'base': { 'reboot': { - 'parameters': {'delay': {'minimum': 10, 'type': 'integer'}}, - 'minimalRole': 'user' + 'parameters': {'delay': {'minimum': 10}}, + 'minimalRole': 'user', + 'results': {} } }, 'robot': { '_jump': { - 'parameters': {'_height': {'type': 'integer'}}, - 'minimalRole': 'user' + 'parameters': {'_height': 'integer'}, + 'minimalRole': 'user', + 'results': {} } } })");
diff --git a/src/notification/notification_channel.h b/src/notification/notification_channel.h index ef152a8..5fb7993 100644 --- a/src/notification/notification_channel.h +++ b/src/notification/notification_channel.h
@@ -17,7 +17,7 @@ class NotificationChannel { public: - virtual ~NotificationChannel() {} + virtual ~NotificationChannel() = default; virtual std::string GetName() const = 0; virtual bool IsConnected() const = 0;
diff --git a/src/notification/notification_delegate.h b/src/notification/notification_delegate.h index 263c2f3..719d76d 100644 --- a/src/notification/notification_delegate.h +++ b/src/notification/notification_delegate.h
@@ -24,7 +24,7 @@ virtual void OnDeviceDeleted(const std::string& cloud_id) = 0; protected: - virtual ~NotificationDelegate() {} + virtual ~NotificationDelegate() = default; }; } // namespace weave
diff --git a/src/notification/notification_parser.cc b/src/notification/notification_parser.cc index d7c4f48..0a27f1c 100644 --- a/src/notification/notification_parser.cc +++ b/src/notification/notification_parser.cc
@@ -46,9 +46,9 @@ std::string kind; if (!notification.GetString("kind", &kind) || - kind != "weave#notification") { + kind != "clouddevices#notification") { LOG(WARNING) << "Push notification should have 'kind' property set to " - "weave#notification"; + "clouddevices#notification"; return false; }
diff --git a/src/notification/notification_parser_unittest.cc b/src/notification/notification_parser_unittest.cc index c69f9dd..d0b43fe 100644 --- a/src/notification/notification_parser_unittest.cc +++ b/src/notification/notification_parser_unittest.cc
@@ -6,7 +6,8 @@ #include <gmock/gmock.h> #include <gtest/gtest.h> -#include <weave/test/unittest_utils.h> + +#include "src/commands/unittest_utils.h" using testing::SaveArg; using testing::Invoke; @@ -37,11 +38,11 @@ TEST_F(NotificationParserTest, CommandCreated) { auto json = CreateDictionaryValue(R"({ - "kind": "weave#notification", + "kind": "clouddevices#notification", "type": "COMMAND_CREATED", "deviceId": "device_id", "command": { - "kind": "weave#command", + "kind": "clouddevices#command", "deviceId": "device_id", "state": "queued", "name": "storage.list", @@ -56,7 +57,7 @@ })"); const char expected_json[] = R"({ - "kind": "weave#command", + "kind": "clouddevices#command", "deviceId": "device_id", "state": "queued", "name": "storage.list", @@ -75,7 +76,7 @@ TEST_F(NotificationParserTest, DeviceDeleted) { auto json = CreateDictionaryValue(R"({ - "kind":"weave#notification", + "kind":"clouddevices#notification", "type":"DEVICE_DELETED", "deviceId":"some_device_id" })"); @@ -91,7 +92,7 @@ "type": "COMMAND_CREATED", "deviceId": "device_id", "command": { - "kind": "weave#command", + "kind": "clouddevices#command", "deviceId": "device_id", "state": "queued", "name": "storage.list", @@ -110,10 +111,10 @@ TEST_F(NotificationParserTest, Failure_NoType) { auto json = CreateDictionaryValue(R"({ - "kind": "weave#notification", + "kind": "clouddevices#notification", "deviceId": "device_id", "command": { - "kind": "weave#command", + "kind": "clouddevices#command", "deviceId": "device_id", "state": "queued", "name": "storage.list", @@ -132,11 +133,11 @@ TEST_F(NotificationParserTest, IgnoredNotificationType) { auto json = CreateDictionaryValue(R"({ - "kind": "weave#notification", + "kind": "clouddevices#notification", "type": "COMMAND_EXPIRED", "deviceId": "device_id", "command": { - "kind": "weave#command", + "kind": "clouddevices#command", "deviceId": "device_id", "state": "queued", "name": "storage.list",
diff --git a/src/notification/xmpp_channel.h b/src/notification/xmpp_channel.h index 50e84d2..e6185d9 100644 --- a/src/notification/xmpp_channel.h +++ b/src/notification/xmpp_channel.h
@@ -33,7 +33,7 @@ virtual void SendMessage(const std::string& message) = 0; protected: - virtual ~XmppChannelInterface() {} + virtual ~XmppChannelInterface() = default; }; class XmppChannel : public NotificationChannel,
diff --git a/src/notification/xmpp_iq_stanza_handler.cc b/src/notification/xmpp_iq_stanza_handler.cc index 9ac4223..8753030 100644 --- a/src/notification/xmpp_iq_stanza_handler.cc +++ b/src/notification/xmpp_iq_stanza_handler.cc
@@ -72,7 +72,7 @@ const ResponseCallback& response_callback, const TimeoutCallback& timeout_callback) { // Remember the response callback to call later. - requests_.insert(std::make_pair(++last_request_id_, response_callback)); + requests_.emplace(++last_request_id_, response_callback); // Schedule a time-out callback for this request. if (timeout < base::TimeDelta::Max()) { task_runner_->PostDelayedTask(
diff --git a/src/notification/xmpp_stream_parser.cc b/src/notification/xmpp_stream_parser.cc index 9efaeb5..de3b8f1 100644 --- a/src/notification/xmpp_stream_parser.cc +++ b/src/notification/xmpp_stream_parser.cc
@@ -36,7 +36,7 @@ std::map<std::string, std::string> attributes; if (attr != nullptr) { for (size_t n = 0; attr[n] != nullptr && attr[n + 1] != nullptr; n += 2) { - attributes.insert(std::make_pair(attr[n], attr[n + 1])); + attributes.emplace(attr[n], attr[n + 1]); } } self->OnOpenElement(element, std::move(attributes));
diff --git a/src/notification/xmpp_stream_parser.h b/src/notification/xmpp_stream_parser.h index b8f5723..41faaff 100644 --- a/src/notification/xmpp_stream_parser.h +++ b/src/notification/xmpp_stream_parser.h
@@ -49,7 +49,7 @@ virtual void OnStanza(std::unique_ptr<XmlNode> stanza) = 0; protected: - virtual ~Delegate() {} + virtual ~Delegate() = default; }; explicit XmppStreamParser(Delegate* delegate);
diff --git a/src/privet/cloud_delegate.cc b/src/privet/cloud_delegate.cc index c771366..fddc8e1 100644 --- a/src/privet/cloud_delegate.cc +++ b/src/privet/cloud_delegate.cc
@@ -134,9 +134,7 @@ } std::string GetCloudId() const override { - return connection_state_.status() > ConnectionState::kUnconfigured - ? device_->GetSettings().cloud_id - : ""; + return device_->GetSettings().cloud_id; } const base::DictionaryValue& GetState() const override { return state_; } @@ -213,7 +211,7 @@ private: void OnCommandAdded(Command* command) { // Set to 0 for any new unknown command. - command_owners_.insert(std::make_pair(command->GetID(), 0)); + command_owners_.emplace(command->GetID(), 0); } void OnCommandRemoved(Command* command) { @@ -243,15 +241,17 @@ void OnStateChanged() { state_.Clear(); - const auto& state = state_manager_->GetState(); - state_.MergeDictionary(&state); + auto state = state_manager_->GetState(); + CHECK(state); + state_.MergeDictionary(state.get()); NotifyOnStateChanged(); } void OnCommandDefChanged() { command_defs_.Clear(); - auto commands = - command_manager_->GetCommandDictionary().GetCommandsAsJson(nullptr); + auto commands = command_manager_->GetCommandDictionary().GetCommandsAsJson( + [](const CommandDefinition* def) { return def->GetVisibility().local; }, + true, nullptr); CHECK(commands); command_defs_.MergeDictionary(commands.get()); NotifyOnCommandDefsChanged();
diff --git a/src/privet/cloud_delegate.h b/src/privet/cloud_delegate.h index 6396519..8763fbe 100644 --- a/src/privet/cloud_delegate.h +++ b/src/privet/cloud_delegate.h
@@ -45,7 +45,7 @@ class Observer { public: - virtual ~Observer() {} + virtual ~Observer() = default; virtual void OnDeviceInfoChanged() {} virtual void OnCommandDefsChanged() {}
diff --git a/src/privet/privet_handler.cc b/src/privet/privet_handler.cc index 157feed..157c982 100644 --- a/src/privet/privet_handler.cc +++ b/src/privet/privet_handler.cc
@@ -469,7 +469,7 @@ params.handler = handler; params.scope = scope; params.https_only = false; - CHECK(handlers_.insert(std::make_pair(path, params)).second); + CHECK(handlers_.emplace(path, params).second); } void PrivetHandler::AddSecureHandler(const std::string& path, @@ -479,7 +479,7 @@ params.handler = handler; params.scope = scope; params.https_only = true; - CHECK(handlers_.insert(std::make_pair(path, params)).second); + CHECK(handlers_.emplace(path, params).second); } void PrivetHandler::HandleInfo(const base::DictionaryValue&,
diff --git a/src/privet/security_delegate.h b/src/privet/security_delegate.h index 40f297f..1d28ba3 100644 --- a/src/privet/security_delegate.h +++ b/src/privet/security_delegate.h
@@ -19,7 +19,7 @@ // Interface to provide Security related logic for |PrivetHandler|. class SecurityDelegate { public: - virtual ~SecurityDelegate() {} + virtual ~SecurityDelegate() = default; // Creates access token for the given scope, user id and |time|. virtual std::string CreateAccessToken(const UserInfo& user_info,
diff --git a/src/privet/security_manager.cc b/src/privet/security_manager.cc index 7e35e79..1b4e3c5 100644 --- a/src/privet/security_manager.cc +++ b/src/privet/security_manager.cc
@@ -24,6 +24,10 @@ #include "src/string_utils.h" #include "third_party/chromium/crypto/p224_spake.h" +#if !defined(ARCH_CPU_LITTLE_ENDIAN) +#error "Big-endian is not supported. See b/25017606" +#endif + namespace weave { namespace privet { @@ -254,7 +258,7 @@ } while (confirmed_sessions_.find(session) != confirmed_sessions_.end() || pending_sessions_.find(session) != pending_sessions_.end()); std::string commitment = spake->GetMessage(); - pending_sessions_.insert(std::make_pair(session, std::move(spake))); + pending_sessions_.emplace(session, std::move(spake)); task_runner_->PostDelayedTask( FROM_HERE, @@ -313,8 +317,7 @@ std::vector<uint8_t> cert_hmac = HmacSha256( std::vector<uint8_t>(key.begin(), key.end()), certificate_fingerprint_); *signature = Base64Encode(cert_hmac); - confirmed_sessions_.insert( - std::make_pair(session->first, std::move(session->second))); + confirmed_sessions_.emplace(session->first, std::move(session->second)); task_runner_->PostDelayedTask( FROM_HERE, base::Bind(base::IgnoreResult(&SecurityManager::CloseConfirmedSession),
diff --git a/src/privet/security_manager.h b/src/privet/security_manager.h index bb513e6..c99201b 100644 --- a/src/privet/security_manager.h +++ b/src/privet/security_manager.h
@@ -41,7 +41,7 @@ class KeyExchanger { public: - virtual ~KeyExchanger() {} + virtual ~KeyExchanger() = default; virtual const std::string& GetMessage() = 0; virtual bool ProcessMessage(const std::string& message,
diff --git a/src/privet/security_manager_unittest.cc b/src/privet/security_manager_unittest.cc index 0880b14..7229710 100644 --- a/src/privet/security_manager_unittest.cc +++ b/src/privet/security_manager_unittest.cc
@@ -109,7 +109,7 @@ }; TEST_F(SecurityManagerTest, RandomSecret) { - EXPECT_GE(security_.GetSecret().size(), 32u); + EXPECT_GE(security_.GetSecret().size(), 32); EXPECT_TRUE(IsBase64(security_.GetSecret())); }
diff --git a/src/privet/wifi_delegate.h b/src/privet/wifi_delegate.h index 9bd5157..ae5e520 100644 --- a/src/privet/wifi_delegate.h +++ b/src/privet/wifi_delegate.h
@@ -18,7 +18,7 @@ class WifiDelegate { public: WifiDelegate() = default; - virtual ~WifiDelegate() {} + virtual ~WifiDelegate() = default; // Returns status of the WiFi connection. virtual const ConnectionState& GetConnectionState() const = 0;
diff --git a/src/privet/wifi_ssid_generator_unittest.cc b/src/privet/wifi_ssid_generator_unittest.cc index 10680c8..398e105 100644 --- a/src/privet/wifi_ssid_generator_unittest.cc +++ b/src/privet/wifi_ssid_generator_unittest.cc
@@ -23,7 +23,7 @@ }; TEST_F(WifiSsidGeneratorTest, GenerateFlagsNoHostedAp) { - EXPECT_EQ(ssid_generator_.GenerateFlags().size(), 2u); + EXPECT_EQ(ssid_generator_.GenerateFlags().size(), 2); wifi_.connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; gcd_.connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; @@ -42,7 +42,7 @@ TEST_F(WifiSsidGeneratorTest, GenerateFlagsWithHostedAp) { EXPECT_CALL(wifi_, GetHostedSsid()) .WillRepeatedly(Return(ssid_generator_.GenerateSsid())); - EXPECT_EQ(ssid_generator_.GenerateFlags().size(), 2u); + EXPECT_EQ(ssid_generator_.GenerateFlags().size(), 2); wifi_.connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; gcd_.connection_state_ = ConnectionState{ConnectionState::kUnconfigured}; @@ -59,7 +59,7 @@ } TEST_F(WifiSsidGeneratorTest, GenerateSsid31orLess) { - EXPECT_LE(ssid_generator_.GenerateSsid().size(), 31u); + EXPECT_LE(ssid_generator_.GenerateSsid().size(), 31); } TEST_F(WifiSsidGeneratorTest, GenerateSsidValue) {
diff --git a/src/states/mock_state_change_queue_interface.h b/src/states/mock_state_change_queue_interface.h index 78c6c82..c3f50c0 100644 --- a/src/states/mock_state_change_queue_interface.h +++ b/src/states/mock_state_change_queue_interface.h
@@ -16,11 +16,9 @@ class MockStateChangeQueueInterface : public StateChangeQueueInterface { public: MOCK_CONST_METHOD0(IsEmpty, bool()); - MOCK_METHOD2(MockNotifyPropertiesUpdated, - bool(base::Time timestamp, - const base::DictionaryValue& changed_properties)); - MOCK_METHOD0(MockGetAndClearRecordedStateChanges, - std::vector<StateChange>&()); + MOCK_METHOD2(NotifyPropertiesUpdated, + bool(base::Time timestamp, ValueMap changed_properties)); + MOCK_METHOD0(GetAndClearRecordedStateChanges, std::vector<StateChange>()); MOCK_CONST_METHOD0(GetLastStateChangeId, UpdateID()); MOCK_METHOD1(MockAddOnStateUpdatedCallback, base::CallbackList<void(UpdateID)>::Subscription*( @@ -28,15 +26,6 @@ MOCK_METHOD1(NotifyStateUpdatedOnServer, void(UpdateID)); private: - bool NotifyPropertiesUpdated( - base::Time timestamp, - std::unique_ptr<base::DictionaryValue> changed_properties) override { - return MockNotifyPropertiesUpdated(timestamp, *changed_properties); - } - std::vector<StateChange> GetAndClearRecordedStateChanges() override { - return std::move(MockGetAndClearRecordedStateChanges()); - } - Token AddOnStateUpdatedCallback( const base::Callback<void(UpdateID)>& callback) override { return Token{MockAddOnStateUpdatedCallback(callback)};
diff --git a/src/states/state_change_queue.cc b/src/states/state_change_queue.cc index ba7bf65..288dadc 100644 --- a/src/states/state_change_queue.cc +++ b/src/states/state_change_queue.cc
@@ -5,7 +5,6 @@ #include "src/states/state_change_queue.h" #include <base/logging.h> -#include <base/values.h> namespace weave { @@ -14,15 +13,12 @@ CHECK_GT(max_queue_size_, 0U) << "Max queue size must not be zero"; } -bool StateChangeQueue::NotifyPropertiesUpdated( - base::Time timestamp, - std::unique_ptr<base::DictionaryValue> changed_properties) { +bool StateChangeQueue::NotifyPropertiesUpdated(base::Time timestamp, + ValueMap changed_properties) { auto& stored_changes = state_changes_[timestamp]; // Merge the old property set. - if (stored_changes) - stored_changes->MergeDictionary(changed_properties.get()); - else - stored_changes = std::move(changed_properties); + changed_properties.insert(stored_changes.begin(), stored_changes.end()); + stored_changes = std::move(changed_properties); while (state_changes_.size() > max_queue_size_) { // Queue is full. @@ -34,8 +30,8 @@ auto element_old = state_changes_.begin(); auto element_new = std::next(element_old); // This will skip elements that exist in both [old] and [new]. - element_old->second->MergeDictionary(element_new->second.get()); - std::swap(element_old->second, element_new->second); + element_new->second.insert(element_old->second.begin(), + element_old->second.end()); state_changes_.erase(element_old); } ++last_change_id_; @@ -45,8 +41,8 @@ std::vector<StateChange> StateChangeQueue::GetAndClearRecordedStateChanges() { std::vector<StateChange> changes; changes.reserve(state_changes_.size()); - for (auto& pair : state_changes_) { - changes.push_back(StateChange{pair.first, std::move(pair.second)}); + for (const auto& pair : state_changes_) { + changes.emplace_back(pair.first, std::move(pair.second)); } state_changes_.clear(); return changes;
diff --git a/src/states/state_change_queue.h b/src/states/state_change_queue.h index 314f746..00b827f 100644 --- a/src/states/state_change_queue.h +++ b/src/states/state_change_queue.h
@@ -21,9 +21,8 @@ // Overrides from StateChangeQueueInterface. bool IsEmpty() const override { return state_changes_.empty(); } - bool NotifyPropertiesUpdated( - base::Time timestamp, - std::unique_ptr<base::DictionaryValue> changed_properties) override; + bool NotifyPropertiesUpdated(base::Time timestamp, + ValueMap changed_properties) override; std::vector<StateChange> GetAndClearRecordedStateChanges() override; UpdateID GetLastStateChangeId() const override { return last_change_id_; } Token AddOnStateUpdatedCallback( @@ -36,7 +35,7 @@ const size_t max_queue_size_; // Accumulated list of device state change notifications. - std::map<base::Time, std::unique_ptr<base::DictionaryValue>> state_changes_; + std::map<base::Time, ValueMap> state_changes_; // An ID of last state change update. Each NotifyPropertiesUpdated() // invocation increments this value by 1.
diff --git a/src/states/state_change_queue_interface.h b/src/states/state_change_queue_interface.h index 631a127..e3b3650 100644 --- a/src/states/state_change_queue_interface.h +++ b/src/states/state_change_queue_interface.h
@@ -10,9 +10,7 @@ #include <base/callback_list.h> #include <base/time/time.h> -namespace base { -class DictionaryValue; -} // namespace base +#include "src/commands/schema_utils.h" namespace weave { @@ -21,11 +19,10 @@ // |changed_properties| contains a property set with the new property values // which were updated at the time the event was recorded. struct StateChange { - StateChange(base::Time time, - std::unique_ptr<base::DictionaryValue> properties) + StateChange(base::Time time, ValueMap properties) : timestamp{time}, changed_properties{std::move(properties)} {} base::Time timestamp; - std::unique_ptr<base::DictionaryValue> changed_properties; + ValueMap changed_properties; }; // An abstract interface to StateChangeQueue to record and retrieve state @@ -40,9 +37,8 @@ virtual bool IsEmpty() const = 0; // Called by StateManager when device state properties are updated. - virtual bool NotifyPropertiesUpdated( - base::Time timestamp, - std::unique_ptr<base::DictionaryValue> changed_properties) = 0; + virtual bool NotifyPropertiesUpdated(base::Time timestamp, + ValueMap changed_properties) = 0; // Returns the recorded state changes since last time this method was called. virtual std::vector<StateChange> GetAndClearRecordedStateChanges() = 0;
diff --git a/src/states/state_change_queue_unittest.cc b/src/states/state_change_queue_unittest.cc index bb2947a..9f26071 100644 --- a/src/states/state_change_queue_unittest.cc +++ b/src/states/state_change_queue_unittest.cc
@@ -5,14 +5,12 @@ #include "src/states/state_change_queue.h" #include <gtest/gtest.h> -#include <weave/test/unittest_utils.h> #include "src/bind_lambda.h" +#include "src/commands/unittest_utils.h" namespace weave { -using test::CreateDictionaryValue; - class StateChangeQueueTest : public ::testing::Test { public: void SetUp() override { queue_.reset(new StateChangeQueue(100)); } @@ -24,44 +22,48 @@ TEST_F(StateChangeQueueTest, Empty) { EXPECT_TRUE(queue_->IsEmpty()); - EXPECT_EQ(0u, queue_->GetLastStateChangeId()); + EXPECT_EQ(0, queue_->GetLastStateChangeId()); EXPECT_TRUE(queue_->GetAndClearRecordedStateChanges().empty()); } TEST_F(StateChangeQueueTest, UpdateOne) { - auto timestamp = base::Time::Now(); - ASSERT_TRUE(queue_->NotifyPropertiesUpdated( - timestamp, CreateDictionaryValue("{'prop': {'name': 23}}"))); + StateChange change{base::Time::Now(), + ValueMap{{"prop.name", test::make_int_prop_value(23)}}}; + ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change.timestamp, + change.changed_properties)); EXPECT_FALSE(queue_->IsEmpty()); - EXPECT_EQ(1u, queue_->GetLastStateChangeId()); + EXPECT_EQ(1, queue_->GetLastStateChangeId()); auto changes = queue_->GetAndClearRecordedStateChanges(); - EXPECT_EQ(1u, queue_->GetLastStateChangeId()); - ASSERT_EQ(1u, changes.size()); - EXPECT_EQ(timestamp, changes.front().timestamp); - EXPECT_JSON_EQ("{'prop':{'name': 23}}", *changes.front().changed_properties); + EXPECT_EQ(1, queue_->GetLastStateChangeId()); + ASSERT_EQ(1, changes.size()); + EXPECT_EQ(change.timestamp, changes.front().timestamp); + EXPECT_EQ(change.changed_properties, changes.front().changed_properties); EXPECT_TRUE(queue_->IsEmpty()); EXPECT_TRUE(queue_->GetAndClearRecordedStateChanges().empty()); } -TEST_F(StateChangeQueueTest, UpdateMany) { - auto timestamp1 = base::Time::Now(); - const std::string state1 = "{'prop': {'name1': 23}}"; - auto timestamp2 = timestamp1 + base::TimeDelta::FromSeconds(1); - const std::string state2 = - "{'prop': {'name1': 17, 'name2': 1.0, 'name3': false}}"; - ASSERT_TRUE(queue_->NotifyPropertiesUpdated( - timestamp1, CreateDictionaryValue(state1))); - ASSERT_TRUE(queue_->NotifyPropertiesUpdated( - timestamp2, CreateDictionaryValue(state2))); - - EXPECT_EQ(2u, queue_->GetLastStateChangeId()); +// TODO(vitalybuka): Fix flakiness. +TEST_F(StateChangeQueueTest, DISABLED_UpdateMany) { + StateChange change1{base::Time::Now(), + ValueMap{{"prop.name1", test::make_int_prop_value(23)}}}; + ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change1.timestamp, + change1.changed_properties)); + StateChange change2{base::Time::Now(), + ValueMap{ + {"prop.name1", test::make_int_prop_value(17)}, + {"prop.name2", test::make_double_prop_value(1.0)}, + {"prop.name3", test::make_bool_prop_value(false)}, + }}; + ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change2.timestamp, + change2.changed_properties)); + EXPECT_EQ(2, queue_->GetLastStateChangeId()); EXPECT_FALSE(queue_->IsEmpty()); auto changes = queue_->GetAndClearRecordedStateChanges(); - ASSERT_EQ(2u, changes.size()); - EXPECT_EQ(timestamp1, changes[0].timestamp); - EXPECT_JSON_EQ(state1, *changes[0].changed_properties); - EXPECT_EQ(timestamp2, changes[1].timestamp); - EXPECT_JSON_EQ(state2, *changes[1].changed_properties); + ASSERT_EQ(2, changes.size()); + EXPECT_EQ(change1.timestamp, changes[0].timestamp); + EXPECT_EQ(change1.changed_properties, changes[0].changed_properties); + EXPECT_EQ(change2.timestamp, changes[1].timestamp); + EXPECT_EQ(change2.changed_properties, changes[1].changed_properties); EXPECT_TRUE(queue_->IsEmpty()); EXPECT_TRUE(queue_->GetAndClearRecordedStateChanges().empty()); } @@ -71,27 +73,33 @@ base::TimeDelta time_delta = base::TimeDelta::FromMinutes(1); ASSERT_TRUE(queue_->NotifyPropertiesUpdated( - timestamp, CreateDictionaryValue("{'prop': {'name1': 1}}"))); + timestamp, ValueMap{{"prop.name1", test::make_int_prop_value(1)}})); ASSERT_TRUE(queue_->NotifyPropertiesUpdated( - timestamp, CreateDictionaryValue("{'prop': {'name2': 2}}"))); + timestamp, ValueMap{{"prop.name2", test::make_int_prop_value(2)}})); ASSERT_TRUE(queue_->NotifyPropertiesUpdated( - timestamp, CreateDictionaryValue("{'prop': {'name1': 3}}"))); + timestamp, ValueMap{{"prop.name1", test::make_int_prop_value(3)}})); ASSERT_TRUE(queue_->NotifyPropertiesUpdated( - timestamp + time_delta, CreateDictionaryValue("{'prop': {'name1': 4}}"))); + timestamp + time_delta, + ValueMap{{"prop.name1", test::make_int_prop_value(4)}})); auto changes = queue_->GetAndClearRecordedStateChanges(); - EXPECT_EQ(4u, queue_->GetLastStateChangeId()); - ASSERT_EQ(2u, changes.size()); + EXPECT_EQ(4, queue_->GetLastStateChangeId()); + ASSERT_EQ(2, changes.size()); - const std::string expected1 = "{'prop': {'name1': 3, 'name2': 2}}"; - const std::string expected2 = "{'prop': {'name1': 4}}"; + ValueMap expected1{ + {"prop.name1", test::make_int_prop_value(3)}, + {"prop.name2", test::make_int_prop_value(2)}, + }; + ValueMap expected2{ + {"prop.name1", test::make_int_prop_value(4)}, + }; EXPECT_EQ(timestamp, changes[0].timestamp); - EXPECT_JSON_EQ(expected1, *changes[0].changed_properties); + EXPECT_EQ(expected1, changes[0].changed_properties); EXPECT_EQ(timestamp + time_delta, changes[1].timestamp); - EXPECT_JSON_EQ(expected2, *changes[1].changed_properties); + EXPECT_EQ(expected2, changes[1].changed_properties); } TEST_F(StateChangeQueueTest, MaxQueueSize) { @@ -101,29 +109,43 @@ base::TimeDelta time_delta2 = base::TimeDelta::FromMinutes(3); ASSERT_TRUE(queue_->NotifyPropertiesUpdated( - start_time, CreateDictionaryValue("{'prop': {'name1': 1, 'name2': 2}}"))); + start_time, ValueMap{ + {"prop.name1", test::make_int_prop_value(1)}, + {"prop.name2", test::make_int_prop_value(2)}, + })); ASSERT_TRUE(queue_->NotifyPropertiesUpdated( start_time + time_delta1, - CreateDictionaryValue("{'prop': {'name1': 3, 'name3': 4}}"))); + ValueMap{ + {"prop.name1", test::make_int_prop_value(3)}, + {"prop.name3", test::make_int_prop_value(4)}, + })); ASSERT_TRUE(queue_->NotifyPropertiesUpdated( start_time + time_delta2, - CreateDictionaryValue("{'prop': {'name10': 10, 'name11': 11}}"))); + ValueMap{ + {"prop.name10", test::make_int_prop_value(10)}, + {"prop.name11", test::make_int_prop_value(11)}, + })); - EXPECT_EQ(3u, queue_->GetLastStateChangeId()); + EXPECT_EQ(3, queue_->GetLastStateChangeId()); auto changes = queue_->GetAndClearRecordedStateChanges(); - ASSERT_EQ(2u, changes.size()); + ASSERT_EQ(2, changes.size()); - const std::string expected1 = - "{'prop': {'name1': 3, 'name2': 2, 'name3': 4}}"; + ValueMap expected1{ + {"prop.name1", test::make_int_prop_value(3)}, + {"prop.name2", test::make_int_prop_value(2)}, + {"prop.name3", test::make_int_prop_value(4)}, + }; EXPECT_EQ(start_time + time_delta1, changes[0].timestamp); - EXPECT_JSON_EQ(expected1, *changes[0].changed_properties); + EXPECT_EQ(expected1, changes[0].changed_properties); - const std::string expected2 = - "{'prop': {'name10': 10, 'name11': 11}}"; + ValueMap expected2{ + {"prop.name10", test::make_int_prop_value(10)}, + {"prop.name11", test::make_int_prop_value(11)}, + }; EXPECT_EQ(start_time + time_delta2, changes[1].timestamp); - EXPECT_JSON_EQ(expected2, *changes[1].changed_properties); + EXPECT_EQ(expected2, changes[1].changed_properties); } TEST_F(StateChangeQueueTest, ImmediateStateChangeNotification) { @@ -139,8 +161,10 @@ TEST_F(StateChangeQueueTest, DelayedStateChangeNotification) { // When queue is not empty, registering a new callback will not trigger it. ASSERT_TRUE(queue_->NotifyPropertiesUpdated( - base::Time::Now(), - CreateDictionaryValue("{'prop': {'name1': 1, 'name3': 2}}"))); + base::Time::Now(), ValueMap{ + {"prop.name1", test::make_int_prop_value(1)}, + {"prop.name2", test::make_int_prop_value(2)}, + })); auto callback = [](StateChangeQueueInterface::UpdateID id) { FAIL() << "This should not be called";
diff --git a/src/states/state_manager.cc b/src/states/state_manager.cc index b170b86..fe6e669 100644 --- a/src/states/state_manager.cc +++ b/src/states/state_manager.cc
@@ -27,17 +27,14 @@ callback.Run(); // Force to read current state. } -const base::DictionaryValue& StateManager::GetState() const { - if (!cached_dict_valid_) { - cached_dict_.Clear(); - for (const auto& pair : packages_) { - cached_dict_.SetWithoutPathExpansion( - pair.first, pair.second->GetValuesAsJson().DeepCopy()); - } - cached_dict_valid_ = true; +std::unique_ptr<base::DictionaryValue> StateManager::GetState() const { + std::unique_ptr<base::DictionaryValue> dict{new base::DictionaryValue}; + for (const auto& pair : packages_) { + auto pkg_value = pair.second->GetValuesAsJson(); + CHECK(pkg_value); + dict->SetWithoutPathExpansion(pair.first, pkg_value.release()); } - - return cached_dict_; + return dict; } bool StateManager::SetProperty(const std::string& name, @@ -49,7 +46,8 @@ return result; } -const base::Value* StateManager::GetProperty(const std::string& name) const { +std::unique_ptr<base::Value> StateManager::GetProperty( + const std::string& name) const { auto parts = SplitAtFirst(name, ".", true); const std::string& package_name = parts.first; const std::string& property_name = parts.second; @@ -95,11 +93,8 @@ if (!package->SetPropertyValue(property_name, value, error)) return false; - cached_dict_valid_ = false; - - std::unique_ptr<base::DictionaryValue> prop_set{new base::DictionaryValue}; - prop_set->Set(full_property_name, value.DeepCopy()); - state_change_queue_->NotifyPropertiesUpdated(timestamp, std::move(prop_set)); + ValueMap prop_set{{full_property_name, package->GetProperty(property_name)}}; + state_change_queue_->NotifyPropertiesUpdated(timestamp, prop_set); return true; } @@ -222,9 +217,8 @@ StatePackage* package = FindPackage(package_name); if (package == nullptr) { std::unique_ptr<StatePackage> new_package{new StatePackage(package_name)}; - package = - packages_.insert(std::make_pair(package_name, std::move(new_package))) - .first->second.get(); + package = packages_.emplace(package_name, std::move(new_package)) + .first->second.get(); } return package; }
diff --git a/src/states/state_manager.h b/src/states/state_manager.h index c48a57c..55faf76 100644 --- a/src/states/state_manager.h +++ b/src/states/state_manager.h
@@ -39,11 +39,11 @@ bool LoadStateDefinitionFromJson(const std::string& json, ErrorPtr* error); bool SetProperties(const base::DictionaryValue& dict, ErrorPtr* error); bool SetPropertiesFromJson(const std::string& json, ErrorPtr* error); - const base::Value* GetProperty(const std::string& name) const; + std::unique_ptr<base::Value> GetProperty(const std::string& name) const; bool SetProperty(const std::string& name, const base::Value& value, ErrorPtr* error); - const base::DictionaryValue& GetState() const; + std::unique_ptr<base::DictionaryValue> GetState() const; // Returns the recorded state changes since last time this method has been // called. @@ -80,9 +80,6 @@ std::vector<base::Closure> on_changed_; - mutable base::DictionaryValue cached_dict_; - mutable bool cached_dict_valid_ = false; - DISALLOW_COPY_AND_ASSIGN(StateManager); };
diff --git a/src/states/state_manager_unittest.cc b/src/states/state_manager_unittest.cc index 6899646..3f854fe 100644 --- a/src/states/state_manager_unittest.cc +++ b/src/states/state_manager_unittest.cc
@@ -12,9 +12,9 @@ #include <gmock/gmock.h> #include <gtest/gtest.h> #include <weave/provider/test/mock_config_store.h> -#include <weave/test/unittest_utils.h> #include "src/commands/schema_constants.h" +#include "src/commands/unittest_utils.h" #include "src/states/error_codes.h" #include "src/states/mock_state_change_queue_interface.h" @@ -22,18 +22,17 @@ using testing::_; using testing::Return; -using testing::ReturnRef; using test::CreateDictionaryValue; namespace { const char kBaseDefinition[] = R"({ "base": { - "manufacturer":{"type":"string"}, - "serialNumber":{"type":"string"} + "manufacturer":"string", + "serialNumber":"string" }, "device": { - "state_property":{"type":"string"} + "state_property":"string" } })"; @@ -52,10 +51,6 @@ return CreateDictionaryValue(kBaseDefaults); } -MATCHER_P(IsState, str, "") { - return arg.Equals(CreateDictionaryValue(str).get()); -} - } // anonymous namespace class StateManagerTest : public ::testing::Test { @@ -63,9 +58,9 @@ void SetUp() override { // Initial expectations. EXPECT_CALL(mock_state_change_queue_, IsEmpty()).Times(0); - EXPECT_CALL(mock_state_change_queue_, MockNotifyPropertiesUpdated(_, _)) + EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(_, _)) .WillRepeatedly(Return(true)); - EXPECT_CALL(mock_state_change_queue_, MockGetAndClearRecordedStateChanges()) + EXPECT_CALL(mock_state_change_queue_, GetAndClearRecordedStateChanges()) .Times(0); mgr_.reset(new StateManager(&mock_state_change_queue_)); @@ -107,9 +102,11 @@ 'manufacturer': 'Test Factory', 'serialNumber': 'Test Model' }, - 'device': {} + 'device': { + 'state_property': '' + } })"; - EXPECT_JSON_EQ(expected, mgr_->GetState()); + EXPECT_JSON_EQ(expected, *mgr_->GetState()); } TEST_F(StateManagerTest, LoadStateDefinition) { @@ -125,10 +122,14 @@ 'manufacturer': 'Test Factory', 'serialNumber': 'Test Model' }, - 'power': {}, - 'device': {} + 'power': { + 'battery_level': 0 + }, + 'device': { + 'state_property': '' + } })"; - EXPECT_JSON_EQ(expected, mgr_->GetState()); + EXPECT_JSON_EQ(expected, *mgr_->GetState()); } TEST_F(StateManagerTest, Startup) { @@ -136,15 +137,12 @@ auto state_definition = R"({ "base": { - "firmwareVersion": {"type":"string"}, - "localDiscoveryEnabled": {"type":"boolean"}, - "localAnonymousAccessMaxRole": { - "type": "string", - "enum": ["none", "viewer", "user"] - }, - "localPairingEnabled": {"type":"boolean"} + "firmwareVersion": "string", + "localDiscoveryEnabled": "boolean", + "localAnonymousAccessMaxRole": [ "none", "viewer", "user" ], + "localPairingEnabled": "boolean" }, - "power": {"battery_level":{"type":"integer"}} + "power": {"battery_level":"integer"} })"; ASSERT_TRUE(manager.LoadStateDefinitionFromJson(state_definition, nullptr)); @@ -170,13 +168,15 @@ 'battery_level': 44 } })"; - EXPECT_JSON_EQ(expected, manager.GetState()); + EXPECT_JSON_EQ(expected, *manager.GetState()); } TEST_F(StateManagerTest, SetPropertyValue) { - const std::string state = "{'device': {'state_property': 'Test Value'}}"; + ValueMap expected_prop_set{ + {"device.state_property", test::make_string_prop_value("Test Value")}, + }; EXPECT_CALL(mock_state_change_queue_, - MockNotifyPropertiesUpdated(timestamp_, IsState(state))) + NotifyPropertiesUpdated(timestamp_, expected_prop_set)) .WillOnce(Return(true)); ASSERT_TRUE(SetPropertyValue("device.state_property", base::StringValue{"Test Value"}, nullptr)); @@ -189,7 +189,7 @@ 'state_property': 'Test Value' } })"; - EXPECT_JSON_EQ(expected, mgr_->GetState()); + EXPECT_JSON_EQ(expected, *mgr_->GetState()); } TEST_F(StateManagerTest, SetPropertyValue_Error_NoName) { @@ -216,36 +216,39 @@ } TEST_F(StateManagerTest, SetPropertyValue_Error_UnknownProperty) { - ASSERT_TRUE( - SetPropertyValue("base.level", base::FundamentalValue{0}, nullptr)); + ErrorPtr error; + ASSERT_FALSE( + SetPropertyValue("base.level", base::FundamentalValue{0}, &error)); + EXPECT_EQ(errors::state::kDomain, error->GetDomain()); + EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode()); } TEST_F(StateManagerTest, GetAndClearRecordedStateChanges) { - EXPECT_CALL(mock_state_change_queue_, - MockNotifyPropertiesUpdated(timestamp_, _)) + EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(timestamp_, _)) .WillOnce(Return(true)); ASSERT_TRUE(SetPropertyValue("device.state_property", base::StringValue{"Test Value"}, nullptr)); - std::vector<StateChange> expected_state; - const std::string expected_val = - "{'device': {'state_property': 'Test Value'}}"; - expected_state.emplace_back( - timestamp_, - CreateDictionaryValue(expected_val)); - EXPECT_CALL(mock_state_change_queue_, MockGetAndClearRecordedStateChanges()) - .WillOnce(ReturnRef(expected_state)); + std::vector<StateChange> expected_val; + expected_val.emplace_back( + timestamp_, ValueMap{{"device.state_property", + test::make_string_prop_value("Test Value")}}); + EXPECT_CALL(mock_state_change_queue_, GetAndClearRecordedStateChanges()) + .WillOnce(Return(expected_val)); EXPECT_CALL(mock_state_change_queue_, GetLastStateChangeId()) .WillOnce(Return(0)); auto changes = mgr_->GetAndClearRecordedStateChanges(); - ASSERT_EQ(1u, changes.second.size()); - EXPECT_EQ(timestamp_, changes.second.back().timestamp); - EXPECT_JSON_EQ(expected_val, *changes.second.back().changed_properties); + ASSERT_EQ(1, changes.second.size()); + EXPECT_EQ(expected_val.back().timestamp, changes.second.back().timestamp); + EXPECT_EQ(expected_val.back().changed_properties, + changes.second.back().changed_properties); } TEST_F(StateManagerTest, SetProperties) { - const std::string state = "{'base': {'manufacturer': 'No Name'}}"; + ValueMap expected_prop_set{ + {"base.manufacturer", test::make_string_prop_value("No Name")}, + }; EXPECT_CALL(mock_state_change_queue_, - MockNotifyPropertiesUpdated(_, IsState(state))) + NotifyPropertiesUpdated(_, expected_prop_set)) .WillOnce(Return(true)); EXPECT_CALL(*this, OnStateChanged()).Times(1); @@ -257,14 +260,16 @@ 'manufacturer': 'No Name', 'serialNumber': 'Test Model' }, - 'device': {} + 'device': { + 'state_property': '' + } })"; - EXPECT_JSON_EQ(expected, mgr_->GetState()); + EXPECT_JSON_EQ(expected, *mgr_->GetState()); } TEST_F(StateManagerTest, GetProperty) { EXPECT_JSON_EQ("'Test Model'", *mgr_->GetProperty("base.serialNumber")); - EXPECT_EQ(nullptr, mgr_->GetProperty("device.state_property")); + EXPECT_JSON_EQ("''", *mgr_->GetProperty("device.state_property")); EXPECT_EQ(nullptr, mgr_->GetProperty("device.unknown")); EXPECT_EQ(nullptr, mgr_->GetProperty("unknown.state_property")); }
diff --git a/src/states/state_package.cc b/src/states/state_package.cc index b0ea199..0b8e219 100644 --- a/src/states/state_package.cc +++ b/src/states/state_package.cc
@@ -7,6 +7,9 @@ #include <base/logging.h> #include <base/values.h> +#include "src/commands/prop_types.h" +#include "src/commands/prop_values.h" +#include "src/commands/schema_utils.h" #include "src/states/error_codes.h" namespace weave { @@ -15,18 +18,28 @@ bool StatePackage::AddSchemaFromJson(const base::DictionaryValue* json, ErrorPtr* error) { + ObjectSchema schema; + if (!schema.FromJson(json, nullptr, error)) + return false; + // Scan first to make sure we have no property redefinitions. - for (base::DictionaryValue::Iterator it(*json); !it.IsAtEnd(); it.Advance()) { - if (types_.HasKey(it.key())) { + for (const auto& pair : schema.GetProps()) { + if (types_.GetProp(pair.first)) { Error::AddToPrintf(error, FROM_HERE, errors::state::kDomain, errors::state::kPropertyRedefinition, "State property '%s.%s' is already defined", - name_.c_str(), it.key().c_str()); + name_.c_str(), pair.first.c_str()); return false; } } - types_.MergeDictionary(json); + // Now move all the properties to |types_| object. + for (const auto& pair : schema.GetProps()) { + types_.AddProp(pair.first, pair.second->Clone()); + // Create default value for this state property. + values_.emplace(pair.first, pair.second->CreateDefaultValue()); + } + return true; } @@ -39,15 +52,21 @@ return true; } -const base::DictionaryValue& StatePackage::GetValuesAsJson() const { - return values_; +std::unique_ptr<base::DictionaryValue> StatePackage::GetValuesAsJson() const { + std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + for (const auto& pair : values_) { + auto value = pair.second->ToJson(); + CHECK(value); + dict->SetWithoutPathExpansion(pair.first, value.release()); + } + return dict; } -const base::Value* StatePackage::GetPropertyValue( +std::unique_ptr<base::Value> StatePackage::GetPropertyValue( const std::string& property_name, ErrorPtr* error) const { - const base::Value* value = nullptr; - if (!values_.Get(property_name, &value)) { + auto it = values_.find(property_name); + if (it == values_.end()) { Error::AddToPrintf(error, FROM_HERE, errors::state::kDomain, errors::state::kPropertyNotDefined, "State property '%s.%s' is not defined", name_.c_str(), @@ -55,13 +74,24 @@ return nullptr; } - return value; + return it->second->ToJson(); } bool StatePackage::SetPropertyValue(const std::string& property_name, const base::Value& value, ErrorPtr* error) { - values_.Set(property_name, value.DeepCopy()); + auto it = values_.find(property_name); + if (it == values_.end()) { + Error::AddToPrintf(error, FROM_HERE, errors::state::kDomain, + errors::state::kPropertyNotDefined, + "State property '%s.%s' is not defined", name_.c_str(), + property_name.c_str()); + return false; + } + auto new_value = it->second->GetPropType()->CreatePropValue(value, error); + if (!new_value) + return false; + it->second = std::move(new_value); return true; }
diff --git a/src/states/state_package.h b/src/states/state_package.h index 4092948..e8e3964 100644 --- a/src/states/state_package.h +++ b/src/states/state_package.h
@@ -10,9 +10,15 @@ #include <string> #include <base/macros.h> -#include <base/values.h> #include <weave/error.h> +#include "src/commands/object_schema.h" +#include "src/commands/prop_values.h" + +namespace base { +class DictionaryValue; +} // namespace base + namespace weave { // A package is a set of related state properties. GCD specification defines @@ -43,25 +49,33 @@ // "message": "Printer low on cyan ink" // } // } - const base::DictionaryValue& GetValuesAsJson() const; + std::unique_ptr<base::DictionaryValue> GetValuesAsJson() const; // Gets the value for a specific state property. |property_name| must not // include the package name as part of the property name. - const base::Value* GetPropertyValue(const std::string& property_name, - ErrorPtr* error) const; + std::unique_ptr<base::Value> GetPropertyValue( + const std::string& property_name, + ErrorPtr* error) const; // Sets the value for a specific state property. |property_name| must not // include the package name as part of the property name. bool SetPropertyValue(const std::string& property_name, const base::Value& value, ErrorPtr* error); + std::shared_ptr<const PropValue> GetProperty( + const std::string& property_name) const { + auto it = values_.find(property_name); + return it != values_.end() ? it->second + : std::shared_ptr<const PropValue>{}; + } + // Returns the name of the this package. const std::string& GetName() const { return name_; } private: std::string name_; - base::DictionaryValue types_; - base::DictionaryValue values_; + ObjectSchema types_; + ValueMap values_; friend class StatePackageTestHelper; DISALLOW_COPY_AND_ASSIGN(StatePackage);
diff --git a/src/states/state_package_unittest.cc b/src/states/state_package_unittest.cc index d09625a..f958bf5 100644 --- a/src/states/state_package_unittest.cc +++ b/src/states/state_package_unittest.cc
@@ -9,9 +9,9 @@ #include <base/values.h> #include <gtest/gtest.h> -#include <weave/test/unittest_utils.h> #include "src/commands/schema_constants.h" +#include "src/commands/unittest_utils.h" #include "src/states/error_codes.h" namespace weave { @@ -21,11 +21,11 @@ class StatePackageTestHelper { public: // Returns the state property definitions (types/constraints/etc). - static const base::DictionaryValue& GetTypes(const StatePackage& package) { + static const ObjectSchema& GetTypes(const StatePackage& package) { return package.types_; } // Returns the all state property values in this package. - static const base::DictionaryValue& GetValues(const StatePackage& package) { + static const ValueMap& GetValues(const StatePackage& package) { return package.values_; } }; @@ -33,30 +33,15 @@ namespace { std::unique_ptr<base::DictionaryValue> GetTestSchema() { return CreateDictionaryValue(R"({ - 'color': { - 'type': 'string' + 'light': 'boolean', + 'color': 'string', + 'direction':{ + 'properties':{ + 'azimuth': {'type': 'number', 'isRequired': true}, + 'altitude': {'maximum': 90.0} + } }, - 'direction': { - 'additionalProperties': false, - 'properties': { - 'altitude': { - 'maximum': 90.0, - 'type': 'number' - }, - 'azimuth': { - 'type': 'number' - } - }, - 'type': 'object', - 'required': [ 'azimuth' ] - }, - 'iso': { - 'enum': [50, 100, 200, 400], - 'type': 'integer' - }, - 'light': { - 'type': 'boolean' - } + 'iso': [50, 100, 200, 400] })"); } @@ -69,11 +54,11 @@ })"); } -inline const base::DictionaryValue& GetTypes(const StatePackage& package) { +inline const ObjectSchema& GetTypes(const StatePackage& package) { return StatePackageTestHelper::GetTypes(package); } // Returns the all state property values in this package. -inline const base::DictionaryValue& GetValues(const StatePackage& package) { +inline const ValueMap& GetValues(const StatePackage& package) { return StatePackageTestHelper::GetValues(package); } @@ -93,15 +78,15 @@ TEST(StatePackage, Empty) { StatePackage package("test"); EXPECT_EQ("test", package.GetName()); - EXPECT_TRUE(GetTypes(package).empty()); + EXPECT_TRUE(GetTypes(package).GetProps().empty()); EXPECT_TRUE(GetValues(package).empty()); } TEST(StatePackage, AddSchemaFromJson_OnEmpty) { StatePackage package("test"); ASSERT_TRUE(package.AddSchemaFromJson(GetTestSchema().get(), nullptr)); - EXPECT_EQ(4u, GetTypes(package).size()); - EXPECT_EQ(0u, GetValues(package).size()); + EXPECT_EQ(4, GetTypes(package).GetProps().size()); + EXPECT_EQ(4, GetValues(package).size()); auto expected = R"({ 'color': { @@ -129,16 +114,22 @@ 'type': 'boolean' } })"; - EXPECT_JSON_EQ(expected, GetTypes(package)); + EXPECT_JSON_EQ(expected, *GetTypes(package).ToJson(true, false)); - EXPECT_JSON_EQ("{}", package.GetValuesAsJson()); + expected = R"({ + 'color': '', + 'direction': {}, + 'iso': 0, + 'light': false + })"; + EXPECT_JSON_EQ(expected, *package.GetValuesAsJson()); } TEST(StatePackage, AddValuesFromJson_OnEmpty) { StatePackage package("test"); ASSERT_TRUE(package.AddSchemaFromJson(GetTestSchema().get(), nullptr)); ASSERT_TRUE(package.AddValuesFromJson(GetTestValues().get(), nullptr)); - EXPECT_EQ(4u, GetValues(package).size()); + EXPECT_EQ(4, GetValues(package).size()); auto expected = R"({ 'color': 'white', 'direction': { @@ -148,17 +139,14 @@ 'iso': 200, 'light': true })"; - EXPECT_JSON_EQ(expected, package.GetValuesAsJson()); + EXPECT_JSON_EQ(expected, *package.GetValuesAsJson()); } TEST_F(StatePackageTest, AddSchemaFromJson_AddMore) { - auto dict = CreateDictionaryValue(R"({'brightness':{ - 'enum': ['low', 'medium', 'high'], - 'type': 'string' - }})"); + auto dict = CreateDictionaryValue("{'brightness':['low', 'medium', 'high']}"); ASSERT_TRUE(package_->AddSchemaFromJson(dict.get(), nullptr)); - EXPECT_EQ(5u, GetTypes(*package_).size()); - EXPECT_EQ(4u, GetValues(*package_).size()); + EXPECT_EQ(5, GetTypes(*package_).GetProps().size()); + EXPECT_EQ(5, GetValues(*package_).size()); auto expected = R"({ 'brightness': { 'enum': ['low', 'medium', 'high'], @@ -189,9 +177,10 @@ 'type': 'boolean' } })"; - EXPECT_JSON_EQ(expected, GetTypes(*package_)); + EXPECT_JSON_EQ(expected, *GetTypes(*package_).ToJson(true, false)); expected = R"({ + 'brightness': '', 'color': 'white', 'direction': { 'altitude': 89.9, @@ -200,18 +189,15 @@ 'iso': 200, 'light': true })"; - EXPECT_JSON_EQ(expected, package_->GetValuesAsJson()); + EXPECT_JSON_EQ(expected, *package_->GetValuesAsJson()); } TEST_F(StatePackageTest, AddValuesFromJson_AddMore) { - auto dict = CreateDictionaryValue(R"({'brightness':{ - 'enum': ['low', 'medium', 'high'], - 'type': 'string' - }})"); + auto dict = CreateDictionaryValue("{'brightness':['low', 'medium', 'high']}"); ASSERT_TRUE(package_->AddSchemaFromJson(dict.get(), nullptr)); dict = CreateDictionaryValue("{'brightness':'medium'}"); ASSERT_TRUE(package_->AddValuesFromJson(dict.get(), nullptr)); - EXPECT_EQ(5u, GetValues(*package_).size()); + EXPECT_EQ(5, GetValues(*package_).size()); auto expected = R"({ 'brightness': 'medium', 'color': 'white', @@ -222,12 +208,11 @@ 'iso': 200, 'light': true })"; - EXPECT_JSON_EQ(expected, package_->GetValuesAsJson()); + EXPECT_JSON_EQ(expected, *package_->GetValuesAsJson()); } TEST_F(StatePackageTest, AddSchemaFromJson_Error_Redefined) { - auto dict = CreateDictionaryValue(R"({'color': - {'type':'string', 'enum':['white', 'blue', 'red']}})"); + auto dict = CreateDictionaryValue("{'color':['white', 'blue', 'red']}"); ErrorPtr error; EXPECT_FALSE(package_->AddSchemaFromJson(dict.get(), &error)); EXPECT_EQ(errors::state::kDomain, error->GetDomain()); @@ -236,7 +221,10 @@ TEST_F(StatePackageTest, AddValuesFromJson_Error_Undefined) { auto dict = CreateDictionaryValue("{'brightness':'medium'}"); - EXPECT_TRUE(package_->AddValuesFromJson(dict.get(), nullptr)); + ErrorPtr error; + EXPECT_FALSE(package_->AddValuesFromJson(dict.get(), &error)); + EXPECT_EQ(errors::state::kDomain, error->GetDomain()); + EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode()); } TEST_F(StatePackageTest, GetPropertyValue) { @@ -283,7 +271,88 @@ 'iso': 200, 'light': true })"; - EXPECT_JSON_EQ(expected, package_->GetValuesAsJson()); + EXPECT_JSON_EQ(expected, *package_->GetValuesAsJson()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_TypeMismatch) { + ErrorPtr error; + ASSERT_FALSE( + package_->SetPropertyValue("color", base::FundamentalValue{12}, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); + error.reset(); + + ASSERT_FALSE( + package_->SetPropertyValue("iso", base::FundamentalValue{false}, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_OutOfRange) { + ErrorPtr error; + ASSERT_FALSE( + package_->SetPropertyValue("iso", base::FundamentalValue{150}, &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kOutOfRange, error->GetCode()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Object_TypeMismatch) { + ErrorPtr error; + ASSERT_FALSE(package_->SetPropertyValue( + "direction", + *CreateDictionaryValue("{'altitude': 45.0, 'azimuth': '15'}"), &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kInvalidPropValue, error->GetCode()); + const Error* inner = error->GetInnerError(); + EXPECT_EQ(errors::commands::kDomain, inner->GetDomain()); + EXPECT_EQ(errors::commands::kTypeMismatch, inner->GetCode()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Object_OutOfRange) { + ErrorPtr error; + ASSERT_FALSE(package_->SetPropertyValue( + "direction", + *CreateDictionaryValue("{'altitude': 100.0, 'azimuth': 290.0}"), &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kInvalidPropValue, error->GetCode()); + const Error* inner = error->GetInnerError(); + EXPECT_EQ(errors::commands::kDomain, inner->GetDomain()); + EXPECT_EQ(errors::commands::kOutOfRange, inner->GetCode()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Object_UnknownProperty) { + ErrorPtr error; + ASSERT_FALSE(package_->SetPropertyValue( + "direction", *CreateDictionaryValue( + "{'altitude': 10.0, 'azimuth': 20.0, 'spin': 30.0}"), + &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kUnknownProperty, error->GetCode()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Object_OptionalProperty) { + EXPECT_JSON_EQ("{'altitude': 89.9, 'azimuth': 57.2957795}", + *package_->GetProperty("direction")->ToJson()); + ASSERT_TRUE(package_->SetPropertyValue( + "direction", *CreateDictionaryValue("{'azimuth': 10.0}"), nullptr)); + EXPECT_JSON_EQ("{'azimuth': 10.0}", + *package_->GetProperty("direction")->ToJson()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Object_MissingProperty) { + ErrorPtr error; + ASSERT_FALSE(package_->SetPropertyValue( + "direction", *CreateDictionaryValue("{'altitude': 10.0}"), &error)); + EXPECT_EQ(errors::commands::kDomain, error->GetDomain()); + EXPECT_EQ(errors::commands::kPropertyMissing, error->GetCode()); +} + +TEST_F(StatePackageTest, SetPropertyValue_Error_Unknown) { + ErrorPtr error; + ASSERT_FALSE(package_->SetPropertyValue("volume", base::FundamentalValue{100}, + &error)); + EXPECT_EQ(errors::state::kDomain, error->GetDomain()); + EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode()); } } // namespace weave
diff --git a/src/string_utils_unittest.cc b/src/string_utils_unittest.cc index 3966921..c26d7d8 100644 --- a/src/string_utils_unittest.cc +++ b/src/string_utils_unittest.cc
@@ -17,21 +17,21 @@ std::vector<std::string> parts; parts = Split("", ",", false, false); - EXPECT_EQ(0u, parts.size()); + EXPECT_EQ(0, parts.size()); parts = Split("abc", ",", false, false); - EXPECT_EQ(1u, parts.size()); + EXPECT_EQ(1, parts.size()); EXPECT_EQ("abc", parts[0]); parts = Split(",a,bc , d, ,e, ", ",", true, true); - EXPECT_EQ(4u, parts.size()); + EXPECT_EQ(4, parts.size()); EXPECT_EQ("a", parts[0]); EXPECT_EQ("bc", parts[1]); EXPECT_EQ("d", parts[2]); EXPECT_EQ("e", parts[3]); parts = Split(",a,bc , d, ,e, ", ",", false, true); - EXPECT_EQ(6u, parts.size()); + EXPECT_EQ(6, parts.size()); EXPECT_EQ("a", parts[0]); EXPECT_EQ("bc ", parts[1]); EXPECT_EQ(" d", parts[2]); @@ -40,7 +40,7 @@ EXPECT_EQ(" ", parts[5]); parts = Split(",a,bc , d, ,e, ", ",", true, false); - EXPECT_EQ(7u, parts.size()); + EXPECT_EQ(7, parts.size()); EXPECT_EQ("", parts[0]); EXPECT_EQ("a", parts[1]); EXPECT_EQ("bc", parts[2]); @@ -50,7 +50,7 @@ EXPECT_EQ("", parts[6]); parts = Split(",a,bc , d, ,e, ", ",", false, false); - EXPECT_EQ(7u, parts.size()); + EXPECT_EQ(7, parts.size()); EXPECT_EQ("", parts[0]); EXPECT_EQ("a", parts[1]); EXPECT_EQ("bc ", parts[2]); @@ -60,12 +60,12 @@ EXPECT_EQ(" ", parts[6]); parts = Split("abc:=xyz", ":=", false, false); - EXPECT_EQ(2u, parts.size()); + EXPECT_EQ(2, parts.size()); EXPECT_EQ("abc", parts[0]); EXPECT_EQ("xyz", parts[1]); parts = Split("abc", "", false, false); - EXPECT_EQ(3u, parts.size()); + EXPECT_EQ(3, parts.size()); EXPECT_EQ("a", parts[0]); EXPECT_EQ("b", parts[1]); EXPECT_EQ("c", parts[2]);
diff --git a/src/test/mock_command.cc b/src/test/mock_command.cc new file mode 100644 index 0000000..a8564c4 --- /dev/null +++ b/src/test/mock_command.cc
@@ -0,0 +1,30 @@ +// 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 <weave/test/mock_command.h> + +#include <memory> +#include <string> + +#include <base/values.h> + +#include "src/commands/unittest_utils.h" + +namespace weave { +namespace test { + +std::unique_ptr<base::DictionaryValue> MockCommand::GetParameters() const { + return CreateDictionaryValue(MockGetParameters()); +} + +std::unique_ptr<base::DictionaryValue> MockCommand::GetProgress() const { + return CreateDictionaryValue(MockGetProgress()); +} + +std::unique_ptr<base::DictionaryValue> MockCommand::GetResults() const { + return CreateDictionaryValue(MockGetResults()); +} + +} // namespace test +} // namespace weave
diff --git a/src/test/unittest_utils.cc b/src/test/unittest_utils.cc index df8ccc0..b9858e4 100644 --- a/src/test/unittest_utils.cc +++ b/src/test/unittest_utils.cc
@@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include <weave/test/unittest_utils.h> +#include "src/commands/unittest_utils.h" #include <base/json/json_reader.h> #include <base/json/json_writer.h>
diff --git a/src/weave_unittest.cc b/src/weave_unittest.cc index 2e41852..c2f1e8f 100644 --- a/src/weave_unittest.cc +++ b/src/weave_unittest.cc
@@ -72,7 +72,7 @@ "base": { "reboot": { "minimalRole": "user", - "parameters": {"delay": {"type": "integer"}}, + "parameters": {"delay": "integer"}, "results": {} }, "shutdown": {
diff --git a/third_party/chromium/base/build/build_config.h b/third_party/chromium/base/build/build_config.h index 347a636..904965a 100644 --- a/third_party/chromium/base/build/build_config.h +++ b/third_party/chromium/base/build/build_config.h
@@ -147,16 +147,6 @@ #define ARCH_CPU_32_BITS 1 #define ARCH_CPU_LITTLE_ENDIAN 1 #endif -#elif defined(__mips__) -#if defined(__LP64__) -#define ARCH_CPU_MIPS64_FAMILY 1 -#define ARCH_CPU_64_BITS 1 -#define ARCH_CPU_BIG_ENDIAN 1 -#else -#define ARCH_CPU_MIPS_FAMILY 1 -#define ARCH_CPU_32_BITS 1 -#define ARCH_CPU_BIG_ENDIAN 1 -#endif #else #error Please add support for your architecture in build/build_config.h #endif
diff --git a/third_party/chromium/base/time/time_posix.cc b/third_party/chromium/base/time/time_posix.cc index 2825ce5..b625af6 100644 --- a/third_party/chromium/base/time/time_posix.cc +++ b/third_party/chromium/base/time/time_posix.cc
@@ -117,7 +117,7 @@ // => Thu Jan 01 00:00:00 UTC 1970 // irb(main):011:0> Time.at(-11644473600).getutc() // => Mon Jan 01 00:00:00 UTC 1601 -static const int64 kWindowsEpochDeltaSeconds = 11644473600ll; +static const int64 kWindowsEpochDeltaSeconds = INT64_C(11644473600); // static const int64 Time::kWindowsEpochDeltaMicroseconds =
diff --git a/third_party/chromium/base/time/time_unittest.cc b/third_party/chromium/base/time/time_unittest.cc index 508244a..43373e7 100644 --- a/third_party/chromium/base/time/time_unittest.cc +++ b/third_party/chromium/base/time/time_unittest.cc
@@ -584,7 +584,7 @@ exploded.millisecond = 0; Time t = Time::FromUTCExploded(exploded); // Unix 1970 epoch. - EXPECT_EQ(11644473600000000ll, t.ToInternalValue()); + EXPECT_EQ(INT64_C(11644473600000000), t.ToInternalValue()); // We can't test 1601 epoch, since the system time functions on Linux // only compute years starting from 1900.
diff --git a/third_party/modp_b64/modp_b64.cc b/third_party/modp_b64/modp_b64.cc index 8db2ed1..fdb8a40 100644 --- a/third_party/modp_b64/modp_b64.cc +++ b/third_party/modp_b64/modp_b64.cc
@@ -45,12 +45,16 @@ /* public header */ #include "modp_b64.h" -#include <base/build/build_config.h> +/* + * If you are ripping this out of the library, comment out the next + * line and uncomment the next lines as approrpiate + */ +//#include "config.h" -#undef WORDS_BIGENDIAN -#if !defined(ARCH_CPU_LITTLE_ENDIAN) -#define WORDS_BIGENDIAN 1 -#endif +/* if on motoral, sun, ibm; uncomment this */ +/* #define WORDS_BIGENDIAN 1 */ +/* else for Intel, Amd; uncomment this */ +/* #undef WORDS_BIGENDIAN */ #include "modp_b64_data.h" @@ -114,7 +118,7 @@ } #ifdef WORDS_BIGENDIAN /* BIG ENDIAN -- SUN / IBM / MOTOROLA */ -size_t modp_b64_decode(char* dest, const char* src, size_t len) +int modp_b64_decode(char* dest, const char* src, int len) { if (len == 0) return 0;