mirror of
https://github.com/Kitware/CMake.git
synced 2026-08-04 14:50:23 +00:00
string(JSON): Add PARTIAL_EQUAL for subset style JSON comparisons
Fixes: #27641
This commit is contained in:
@@ -59,6 +59,8 @@ Synopsis
|
|||||||
<member|index> [<member|index> ...] <value>)
|
<member|index> [<member|index> ...] <value>)
|
||||||
string(JSON <out-var> [ERROR_VARIABLE <error-var>]
|
string(JSON <out-var> [ERROR_VARIABLE <error-var>]
|
||||||
`EQUAL <JSON-EQUAL_>`__ <json-string1> <json-string2>)
|
`EQUAL <JSON-EQUAL_>`__ <json-string1> <json-string2>)
|
||||||
|
string(JSON <out-var> [ERROR_VARIABLE <error-var>]
|
||||||
|
`PARTIAL_EQUAL <JSON-EQUAL_>`__ <json-string1> <json-string2>)
|
||||||
string(JSON <out-var> [ERROR_VARIABLE <error-var>]
|
string(JSON <out-var> [ERROR_VARIABLE <error-var>]
|
||||||
`STRING_ENCODE <STRING-ENCODE_>`__ <string>)
|
`STRING_ENCODE <STRING-ENCODE_>`__ <string>)
|
||||||
|
|
||||||
@@ -637,6 +639,42 @@ string is passed as a single argument even if it contains semicolons.
|
|||||||
will be set to a true value if the JSON objects are considered equal,
|
will be set to a true value if the JSON objects are considered equal,
|
||||||
or a false value otherwise.
|
or a false value otherwise.
|
||||||
|
|
||||||
|
.. signature::
|
||||||
|
string(JSON <out-var> [ERROR_VARIABLE <error-var>]
|
||||||
|
PARTIAL_EQUAL <json-pattern> <json-actual>)
|
||||||
|
:target: JSON-PARTIAL_EQUAL
|
||||||
|
|
||||||
|
.. versionadded:: 4.4
|
||||||
|
|
||||||
|
Compare two JSON values using *partial* equality. The first argument
|
||||||
|
(``<json-pattern>``) is treated as a pattern to match against the second
|
||||||
|
argument (``<json-actual>``), which is treated as the complete JSON value.
|
||||||
|
The contents of ``<json-pattern>`` and ``<json-actual>`` must be valid JSON.
|
||||||
|
|
||||||
|
The ``<out-var>`` will be set to a true value if ``<json-pattern>`` is
|
||||||
|
contained within ``<json-actual>`` according to the rules below, or a false
|
||||||
|
value otherwise.
|
||||||
|
|
||||||
|
Partial equality is not symmetric. In general,
|
||||||
|
``PARTIAL_EQUAL(A, B)`` and ``PARTIAL_EQUAL(B, A)`` may produce different
|
||||||
|
results.
|
||||||
|
|
||||||
|
Matching rules:
|
||||||
|
|
||||||
|
* **Scalars** (null, number, string, boolean) must match exactly.
|
||||||
|
* **Objects** match if every member in ``<json-pattern>`` exists in
|
||||||
|
``<json-actual>`` and the corresponding values also match recursively.
|
||||||
|
Extra members in ``<json-actual>`` are ignored.
|
||||||
|
* **Arrays** match if every element in ``<json-pattern>`` matches a
|
||||||
|
distinct element in ``<json-actual>`` recursively. Once an element in
|
||||||
|
``<json-actual>`` successfully matches an element in ``<json-pattern>``,
|
||||||
|
it is consumed and cannot be reused to match any subsequent elements.
|
||||||
|
Array element order is not significant. Extra elements in
|
||||||
|
``<json-actual>`` are ignored.
|
||||||
|
|
||||||
|
Type differences do not match (for example, an object does not match an
|
||||||
|
array, and a string does not match a number).
|
||||||
|
|
||||||
.. signature::
|
.. signature::
|
||||||
string(JSON <out-var> [ERROR_VARIABLE <error-var>]
|
string(JSON <out-var> [ERROR_VARIABLE <error-var>]
|
||||||
STRING_ENCODE <string>)
|
STRING_ENCODE <string>)
|
||||||
|
|||||||
5
Help/release/dev/json-partial-equals.rst
Normal file
5
Help/release/dev/json-partial-equals.rst
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
json-partial-equals
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
* The :command:`string(JSON ... PARTIAL_EQUAL) <string(JSON-PARTIAL_EQUAL)>`
|
||||||
|
command was added to compare two JSON values using *partial* equality.
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "cmStringCommand.h"
|
#include "cmStringCommand.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
@@ -928,6 +929,56 @@ std::string WriteJson(Json::Value const& value)
|
|||||||
return Json::writeString(writer, value);
|
return Json::writeString(writer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool JsonPartialMatch(Json::Value const& pattern, Json::Value const& actual)
|
||||||
|
{
|
||||||
|
if (pattern.type() != actual.type()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (pattern.type()) {
|
||||||
|
case Json::nullValue:
|
||||||
|
case Json::intValue:
|
||||||
|
case Json::uintValue:
|
||||||
|
case Json::realValue:
|
||||||
|
case Json::stringValue:
|
||||||
|
case Json::booleanValue:
|
||||||
|
return pattern == actual;
|
||||||
|
|
||||||
|
case Json::objectValue: {
|
||||||
|
std::vector<std::string> const keys = pattern.getMemberNames();
|
||||||
|
return std::all_of(keys.begin(), keys.end(),
|
||||||
|
[&](std::string const& key) {
|
||||||
|
return actual.isMember(key) &&
|
||||||
|
JsonPartialMatch(pattern[key], actual[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case Json::arrayValue: {
|
||||||
|
if (actual.size() < pattern.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::vector<bool> matched(actual.size(), false);
|
||||||
|
for (Json::Value const& p : pattern) {
|
||||||
|
bool found = false;
|
||||||
|
for (Json::ArrayIndex j = 0; j < actual.size(); ++j) {
|
||||||
|
if (matched[j]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (JsonPartialMatch(p, actual[j])) {
|
||||||
|
matched[j] = true;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool HandleJSONCommand(std::vector<std::string> const& arguments,
|
bool HandleJSONCommand(std::vector<std::string> const& arguments,
|
||||||
@@ -954,11 +1005,12 @@ bool HandleJSONCommand(std::vector<std::string> const& arguments,
|
|||||||
auto const& mode = args.PopFront("missing mode argument"_s);
|
auto const& mode = args.PopFront("missing mode argument"_s);
|
||||||
if (mode != "GET"_s && mode != "GET_RAW"_s && mode != "TYPE"_s &&
|
if (mode != "GET"_s && mode != "GET_RAW"_s && mode != "TYPE"_s &&
|
||||||
mode != "MEMBER"_s && mode != "LENGTH"_s && mode != "REMOVE"_s &&
|
mode != "MEMBER"_s && mode != "LENGTH"_s && mode != "REMOVE"_s &&
|
||||||
mode != "SET"_s && mode != "EQUAL"_s && mode != "STRING_ENCODE"_s) {
|
mode != "SET"_s && mode != "EQUAL"_s && mode != "STRING_ENCODE"_s &&
|
||||||
|
mode != "PARTIAL_EQUAL"_s) {
|
||||||
throw json_error(cmStrCat(
|
throw json_error(cmStrCat(
|
||||||
"got an invalid mode '"_s, mode,
|
"got an invalid mode '"_s, mode,
|
||||||
"', expected one of GET, GET_RAW, TYPE, MEMBER, LENGTH, REMOVE, SET, "
|
"', expected one of GET, GET_RAW, TYPE, MEMBER, LENGTH, REMOVE, SET, "
|
||||||
" EQUAL, STRING_ENCODE"_s));
|
" EQUAL, PARTIAL_EQUAL, STRING_ENCODE"_s));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto const& jsonstr = args.PopFront("missing json string argument"_s);
|
auto const& jsonstr = args.PopFront("missing json string argument"_s);
|
||||||
@@ -1067,6 +1119,12 @@ bool HandleJSONCommand(std::vector<std::string> const& arguments,
|
|||||||
args.PopFront("missing second json string argument"_s);
|
args.PopFront("missing second json string argument"_s);
|
||||||
Json::Value json2 = ReadJson(jsonstr2);
|
Json::Value json2 = ReadJson(jsonstr2);
|
||||||
makefile.AddDefinitionBool(*outputVariable, json == json2);
|
makefile.AddDefinitionBool(*outputVariable, json == json2);
|
||||||
|
} else if (mode == "PARTIAL_EQUAL"_s) {
|
||||||
|
auto const& jsonstr2 =
|
||||||
|
args.PopFront("missing second json string argument"_s);
|
||||||
|
Json::Value json2 = ReadJson(jsonstr2);
|
||||||
|
makefile.AddDefinitionBool(*outputVariable,
|
||||||
|
JsonPartialMatch(json, json2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -393,3 +393,320 @@ string(JSON result STRING_ENCODE {})
|
|||||||
assert_strequal("${result}" "\"{}\"")
|
assert_strequal("${result}" "\"{}\"")
|
||||||
string(JSON result STRING_ENCODE [])
|
string(JSON result STRING_ENCODE [])
|
||||||
assert_strequal("${result}" "\"[]\"")
|
assert_strequal("${result}" "\"[]\"")
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"foo":"bar"
|
||||||
|
}
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"foo": "bar",
|
||||||
|
"extra": 1
|
||||||
|
}
|
||||||
|
]=])
|
||||||
|
|
||||||
|
if(NOT result)
|
||||||
|
message(SEND_ERROR "Expected ON got ${result}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"foo":"bar"
|
||||||
|
}
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"foo1": "bar"
|
||||||
|
}
|
||||||
|
]=])
|
||||||
|
if(result)
|
||||||
|
message(SEND_ERROR "EXPECTED OFF got ${result}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"types" : {
|
||||||
|
"number" : 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"foo" : "bar",
|
||||||
|
"array" : [5, "val", {"some": "other"}, null],
|
||||||
|
"types" : {
|
||||||
|
"null" : null,
|
||||||
|
"number" : 5,
|
||||||
|
"string" : "foo",
|
||||||
|
"boolean" : false,
|
||||||
|
"array" : [1,2,3],
|
||||||
|
"object" : {}
|
||||||
|
},
|
||||||
|
"values" : {
|
||||||
|
"null" : null,
|
||||||
|
"number" : 5,
|
||||||
|
"string" : "foo",
|
||||||
|
"false" : false,
|
||||||
|
"true" : true
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"foo": "bar"
|
||||||
|
},
|
||||||
|
"special" : {
|
||||||
|
"foo;bar" : "value1",
|
||||||
|
";" : "value2",
|
||||||
|
"semicolon" : ";",
|
||||||
|
"list" : ["one", "two;three", "four"],
|
||||||
|
"quote" : "\"",
|
||||||
|
"\"" : "quote",
|
||||||
|
"backslash" : "\\",
|
||||||
|
"\\" : "backslash",
|
||||||
|
"slash" : "\/",
|
||||||
|
"\/" : "slash",
|
||||||
|
"newline" : "\n",
|
||||||
|
"\n" : "newline",
|
||||||
|
"return" : "\r",
|
||||||
|
"\r" : "return",
|
||||||
|
"tab" : "\t",
|
||||||
|
"\t" : "tab",
|
||||||
|
"backspace" : "\b",
|
||||||
|
"\b" : "backspace",
|
||||||
|
"formfeed" : "\f",
|
||||||
|
"\f" : "formfeed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]=])
|
||||||
|
if(NOT result)
|
||||||
|
message(SEND_ERROR "EXPECTED ON got ${result} for nested subset")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"types" : {
|
||||||
|
"number" : 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"foo" : "bar",
|
||||||
|
"array" : [5, "val", {"some": "other"}, null],
|
||||||
|
"types" : {
|
||||||
|
"null" : null,
|
||||||
|
"number" : 5,
|
||||||
|
"string" : "foo",
|
||||||
|
"boolean" : false,
|
||||||
|
"array" : [1,2,3],
|
||||||
|
"object" : {}
|
||||||
|
},
|
||||||
|
"values" : {
|
||||||
|
"null" : null,
|
||||||
|
"number" : 5,
|
||||||
|
"string" : "foo",
|
||||||
|
"false" : false,
|
||||||
|
"true" : true
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"foo": "bar"
|
||||||
|
},
|
||||||
|
"special" : {
|
||||||
|
"foo;bar" : "value1",
|
||||||
|
";" : "value2",
|
||||||
|
"semicolon" : ";",
|
||||||
|
"list" : ["one", "two;three", "four"],
|
||||||
|
"quote" : "\"",
|
||||||
|
"\"" : "quote",
|
||||||
|
"backslash" : "\\",
|
||||||
|
"\\" : "backslash",
|
||||||
|
"slash" : "\/",
|
||||||
|
"\/" : "slash",
|
||||||
|
"newline" : "\n",
|
||||||
|
"\n" : "newline",
|
||||||
|
"return" : "\r",
|
||||||
|
"\r" : "return",
|
||||||
|
"tab" : "\t",
|
||||||
|
"\t" : "tab",
|
||||||
|
"backspace" : "\b",
|
||||||
|
"\b" : "backspace",
|
||||||
|
"formfeed" : "\f",
|
||||||
|
"\f" : "formfeed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]=])
|
||||||
|
if(result)
|
||||||
|
message(SEND_ERROR "EXPECTED OFF got ${result} for nested mismatch")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
]=])
|
||||||
|
if(NOT result)
|
||||||
|
message(SEND_ERROR "Expected ON got ${result} for unordered array subset")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
4
|
||||||
|
]
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
]=])
|
||||||
|
if(result)
|
||||||
|
message(SEND_ERROR "Expected OFF got ${result} for missing array element")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
]=])
|
||||||
|
if(result)
|
||||||
|
message(SEND_ERROR "Expected OFF got ${result} for pattern larger than actual")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
]=])
|
||||||
|
if(NOT result)
|
||||||
|
message(SEND_ERROR "EXPECTED ON got ${result} for duplicate match")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id":2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id":1,
|
||||||
|
"name":"A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":2,
|
||||||
|
"name":"B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":3,
|
||||||
|
"name":"C"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]=])
|
||||||
|
if(NOT result)
|
||||||
|
message(SEND_ERROR "EXPECTED ON got ${result} for array of object subset")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id":4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id":1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]=])
|
||||||
|
if(result)
|
||||||
|
message(SEND_ERROR "Expected OFF got ${result} for array of object mismatch")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"tags": [
|
||||||
|
"b",
|
||||||
|
"a"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"tags": [
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
"other": 123
|
||||||
|
}
|
||||||
|
]=])
|
||||||
|
if(NOT result)
|
||||||
|
message(SEND_ERROR "EXPECTED ON got ${result} for object+array partial match")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(JSON result PARTIAL_EQUAL
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"tags": [
|
||||||
|
"a",
|
||||||
|
"a"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]=]
|
||||||
|
[=[
|
||||||
|
{
|
||||||
|
"tags": [
|
||||||
|
"a",
|
||||||
|
"b"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]=])
|
||||||
|
if(result)
|
||||||
|
message(SEND_ERROR "Expected OFF got ${result} for duplicate array mismatch in object")
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
CMake Error at JSONWrongMode\.cmake:1 \(string\):
|
CMake Error at JSONWrongMode\.cmake:1 \(string\):
|
||||||
string sub-command JSON got an invalid mode 'FOO', expected one of GET,
|
string sub-command JSON got an invalid mode 'FOO', expected one of GET,
|
||||||
GET_RAW, TYPE, MEMBER, LENGTH, REMOVE, SET, EQUAL, STRING_ENCODE\.
|
GET_RAW, TYPE, MEMBER, LENGTH, REMOVE, SET, EQUAL, PARTIAL_EQUAL,
|
||||||
|
STRING_ENCODE\.
|
||||||
Call Stack \(most recent call first\):
|
Call Stack \(most recent call first\):
|
||||||
CMakeLists\.txt:[0-9]+ \(include\)
|
CMakeLists\.txt:[0-9]+ \(include\)
|
||||||
|
|||||||
Reference in New Issue
Block a user