diff --git a/cmake/modules/yaml.cmake b/cmake/modules/yaml.cmake index 2396e3f08d7..ec12a04bec5 100644 --- a/cmake/modules/yaml.cmake +++ b/cmake/modules/yaml.cmake @@ -19,6 +19,13 @@ # - foo2 # - foo3 # +# Support for list of maps, like: +# foo: +# - bar: val1 +# baz: val1 +# - bar: val2 +# baz: val2 +# # All of above can be combined, for example like: # foo: # bar: baz @@ -28,14 +35,6 @@ # - beta # - gamma # fred: thud -# -# Support for list of objects are currently experimental and not guranteed to work. -# For example: -# foo: -# - bar: val1 -# baz: val1 -# - bar: val2 -# baz: val2 include_guard(GLOBAL) @@ -97,6 +96,13 @@ function(internal_yaml_list_append var genex key) internal_yaml_list_initializer(subjson TRUE) if(${arraylength} GREATER 0) math(EXPR arraystop "${arraylength} - 1") + list(GET ARG_YAML_LIST 0 entry_0) + if(entry_0 STREQUAL MAP) + message(FATAL_ERROR "${function}(GENEX ${argument} ) is not valid at this position.\n" + "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"" + ) + endif() + foreach(i RANGE 0 ${arraystop}) string(JSON item GET "${json_content}" ${key} ${i}) list(APPEND subjson ${item}) @@ -109,12 +115,49 @@ function(internal_yaml_list_append var genex key) # lists are stored as JSON arrays string(JSON index LENGTH "${subjson}") list(LENGTH ARGN length) - math(EXPR stop "${index} + ${length} - 1") if(NOT length EQUAL 0) - foreach(i RANGE ${index} ${stop}) - list(POP_FRONT ARGN value) - string(JSON json_content SET "${json_content}" ${key} ${i} "\"${value}\"") - endforeach() + list(GET ARG_YAML_LIST 0 entry_0) + if(entry_0 STREQUAL MAP) + math(EXPR length "${length} / 2") + math(EXPR stop "${index} + ${length} - 1") + foreach(i RANGE ${index} ${stop}) + list(POP_FRONT ARG_YAML_LIST argument) + if(NOT argument STREQUAL MAP) + message(FATAL_ERROR "yaml_set(${argument} ) is not valid at this position.\n" + "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"" + ) + endif() + list(POP_FRONT ARG_YAML_LIST map_value) + string(REGEX REPLACE "([^\\])," "\\1;" pair_list "${map_value}") + set(qouted_map_value) + foreach(pair ${pair_list}) + if(NOT pair MATCHES "[^ ]*:[^ ]*") + message(FATAL_ERROR "yaml_set(MAP ${map_value} ) is malformed.\n" + "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"\n" + "If value contains comma ',' then ensure the value field is properly qouted " + "and escaped" + ) + endif() + string(REGEX MATCH "^[^:]*" map_key "${pair}") + string(REGEX REPLACE "^${map_key}:[ ]*" "" value "${pair}") + string(STRIP "${map_key}" map_key) + if(value MATCHES "," AND NOT (value MATCHES "\\\\," AND value MATCHES "'.*'")) + message(FATAL_ERROR "value: ${value} is not properly quoted") + endif() + string(REGEX REPLACE "\\\\," "," value "${value}") + list(APPEND qouted_map_value "\"${map_key}\": \"${value}\"") + endforeach() + list(JOIN qouted_map_value "," qouted_map_value) + string(JSON json_content SET "${json_content}" ${key} ${i} "{${qouted_map_value}}") + endforeach() + else() + math(EXPR stop "${index} + ${length} - 1") + list(GET ARG_YAML_LIST 0 entry_0) + foreach(i RANGE ${index} ${stop}) + list(POP_FRONT ARGN value) + string(JSON json_content SET "${json_content}" ${key} ${i} "\"${value}\"") + endforeach() + endif() endif() endif() set(${var} "${json_content}" PARENT_SCOPE) @@ -293,6 +336,7 @@ endfunction() # Usage: # yaml_set(NAME KEY ... [GENEX] VALUE ) # yaml_set(NAME KEY ... [APPEND] [GENEX] LIST ...) +# yaml_set(NAME KEY ... [APPEND] LIST MAP MAP MAP ...) # # Set a value or a list of values to given key. # @@ -306,6 +350,14 @@ endfunction() # APPEND : Append the list of values to the list of values for the key. # GENEX : The value(s) contain generator expressions. When using this # option, also see the notes in the yaml_save() function. +# MAP : Map, with key-value pairs where key-value is separated by ':', +# and pairs separated by ','. +# Format example: ": , : , ..." +# MAP can be given multiple times to separate maps when adding them to a list. +# LIST MAP cannot be used with GENEX. +# +# Note: if a map value contains commas, ',', then the value string must be quoted in +# single quotes and commas must be double escaped, like this: 'A \\,string' # function(yaml_set) cmake_parse_arguments(ARG_YAML "APPEND;GENEX" "NAME;VALUE" "KEY;LIST" ${ARGN}) @@ -328,6 +380,10 @@ function(yaml_set) message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION}(APPEND ...) can only be used with argument: LIST") endif() + if(ARG_YAML_GENEX AND MAP IN_LIST ARG_YAML_LIST) + message(FATAL_ERROR "${function}(GENEX ...) cannot be used with argument: LIST MAP") + endif() + zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON) zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX) @@ -366,7 +422,6 @@ function(yaml_set) internal_yaml_list_initializer(json_string ${genex}) string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "${json_string}") endif() - internal_yaml_list_append(json_content ${genex} "${ARG_YAML_KEY}" ${ARG_YAML_LIST}) else() string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "\"${ARG_YAML_VALUE}\"") @@ -518,7 +573,18 @@ function(to_yaml json level yaml genex) math(EXPR arraystop "${arraylength} - 1") foreach(i RANGE 0 ${arraystop}) string(JSON item GET "${json}" ${member} ${i}) - set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n") + # Check the length of item. Only OBJECT and ARRAY may have length, so a length at this + # level means `to_yaml()` should be called recursively. + string(JSON length ERROR_VARIABLE ignore LENGTH "${item}") + if(length) + set(non_indent_yaml) + to_yaml("${item}" 0 non_indent_yaml FALSE) + string(REGEX REPLACE "\n$" "" non_indent_yaml "${non_indent_yaml}") + string(REPLACE "\n" "\n${indent_${level}} " indent_yaml "${non_indent_yaml}") + set(${yaml} "${${yaml}}${indent_${level}} - ${indent_yaml}\n") + else() + set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n") + endif() endforeach() endif() elseif(type STREQUAL STRING) diff --git a/tests/cmake/yaml/CMakeLists.txt b/tests/cmake/yaml/CMakeLists.txt index 29a89f8a8fc..23e1d55322a 100644 --- a/tests/cmake/yaml/CMakeLists.txt +++ b/tests/cmake/yaml/CMakeLists.txt @@ -109,6 +109,25 @@ function(test_reading_int) ) endfunction() +function(test_reading_map_list_entry) + set(expected_length 2) + set(expected_name "MapEntry1") + set(expected_int 5) + yaml_length(actual_length NAME yaml-test KEY cmake test map-list) + yaml_get(actual_name NAME yaml-test KEY cmake test map-list 0 map-entry-name) + yaml_get(actual_int NAME yaml-test KEY cmake test map-list 0 map-entry-int) + + test_assert(TEST ${expected_length} EQUAL ${actual_length} + COMMENT "yaml key value does not match expectation." + ) + test_assert(TEST ${expected_name} STREQUAL ${actual_name} + COMMENT "yaml key value does not match expectation." + ) + test_assert(TEST ${expected_int} EQUAL ${actual_int} + COMMENT "yaml key value does not match expectation." + ) +endfunction() + function(test_reading_not_found) set(expected cmake-missing-NOTFOUND) yaml_get(actual NAME yaml-test KEY cmake missing test key) @@ -136,6 +155,15 @@ function(test_reading_not_array) ) endfunction() +function(test_reading_not_found_map_list_entry) + set(expected cmake-test-map-list-3-NOTFOUND) + yaml_get(actual NAME yaml-test KEY cmake test map-list 3 map-entry-name) + + test_assert(TEST ${expected} STREQUAL ${actual} + COMMENT "Expected -NOTFOUND, but something was found." + ) +endfunction() + function(test_save_new_file) yaml_save(NAME yaml-test FILE ${CMAKE_BINARY_DIR}/${CMAKE_CURRENT_FUNCTION}_test_save.yaml) @@ -321,6 +349,137 @@ function(test_setting_list_int) endforeach() endfunction() +function(test_setting_map_list_entry) + yaml_create(FILE ${CMAKE_BINARY_DIR}/${CMAKE_CURRENT_FUNCTION}_test_create.yaml + NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create + ) + + set(new_entry_name_0 MapEntryNew1) + set(new_entry_int_0 42) + set(new_entry_name_1 MapEntryNew2) + set(new_entry_int_1 24) + set(new_entry_name_2 MapEntryNew3) + set(new_entry_int_2 4224) + yaml_set(actual NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create + KEY cmake test set map-list LIST + MAP "map-entry-name: ${new_entry_name_0}, map-entry-int: ${new_entry_int_0}" + MAP "map-entry-name: ${new_entry_name_1}, map-entry-int: ${new_entry_int_1}" + MAP "map-entry-name: ${new_entry_name_2}, map-entry-int: ${new_entry_int_2}" + ) + + yaml_save(NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create) + + # Read-back the yaml and verify the values. + yaml_load(FILE ${CMAKE_BINARY_DIR}/${CMAKE_CURRENT_FUNCTION}_test_create.yaml + NAME ${CMAKE_CURRENT_FUNCTION}_readback + ) + + yaml_length(readback NAME ${CMAKE_CURRENT_FUNCTION}_readback KEY cmake test set map-list) + + test_assert(TEST 3 EQUAL ${readback} + COMMENT "readback yaml list length does not match original." + ) + + foreach(index 0 1 2) + yaml_get(readback_name NAME ${CMAKE_CURRENT_FUNCTION}_readback KEY cmake test set map-list ${index} map-entry-name) + yaml_get(readback_int NAME ${CMAKE_CURRENT_FUNCTION}_readback KEY cmake test set map-list ${index} map-entry-int) + + test_assert(TEST "${readback_name}" STREQUAL "${new_entry_name_${index}}" + COMMENT "list values mismatch." + ) + test_assert(TEST "${readback_int}" EQUAL "${new_entry_int_${index}}" + COMMENT "list values mismatch." + ) + endforeach() +endfunction() + +function(test_setting_map_list_entry_windows) + yaml_create(FILE ${CMAKE_BINARY_DIR}/${CMAKE_CURRENT_FUNCTION}_test_create.yaml + NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create + ) + + set(new_entry_name_0 MapEntryWindowsPath1) + set(new_entry_path_0 "c:/tmp/zephyr") + set(new_entry_name_1 MapEntryWindowsPath2) + set(new_entry_path_1 "c:/program files/space") + set(new_entry_name_2 MapEntryWindowsPath3) + set(new_entry_path_2 "D:/alternative/drive") + yaml_set(actual NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create + KEY cmake test set map-list LIST + MAP "map-entry-name: ${new_entry_name_0}, map-entry-path: ${new_entry_path_0}" + MAP "map-entry-name: ${new_entry_name_1}, map-entry-path: ${new_entry_path_1}" + MAP "map-entry-name: ${new_entry_name_2}, map-entry-path: ${new_entry_path_2}" + ) + + yaml_save(NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create) + + # Read-back the yaml and verify the values. + yaml_load(FILE ${CMAKE_BINARY_DIR}/${CMAKE_CURRENT_FUNCTION}_test_create.yaml + NAME ${CMAKE_CURRENT_FUNCTION}_readback + ) + + yaml_length(readback NAME ${CMAKE_CURRENT_FUNCTION}_readback KEY cmake test set map-list) + + test_assert(TEST 3 EQUAL ${readback} + COMMENT "readback yaml list length does not match original." + ) + + foreach(index 0 1 2) + yaml_get(readback_name NAME ${CMAKE_CURRENT_FUNCTION}_readback KEY cmake test set map-list ${index} map-entry-name) + yaml_get(readback_path NAME ${CMAKE_CURRENT_FUNCTION}_readback KEY cmake test set map-list ${index} map-entry-path) + + test_assert(TEST "${readback_name}" STREQUAL "${new_entry_name_${index}}" + COMMENT "list values mismatch." + ) + test_assert(TEST "${readback_path}" STREQUAL "${new_entry_path_${index}}" + COMMENT "list values mismatch." + ) + endforeach() +endfunction() + +function(test_setting_map_list_entry_commas) + yaml_create(FILE ${CMAKE_BINARY_DIR}/${CMAKE_CURRENT_FUNCTION}_test_create.yaml + NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create + ) + + set(new_entry_name_0 TestString1) + set(new_entry_str_0 "'A\\,string'") + set(new_entry_name_1 TestString2) + set(new_entry_str_1 "'\\, is first'") + set(new_entry_name_2 TestString3) + set(new_entry_str_2 "'\\, and : is\\,everywhere\\,'") + yaml_set(actual NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create + KEY cmake test set map-list LIST + MAP "map-entry-name: ${new_entry_name_0}, map-entry-str: ${new_entry_str_0}" + MAP "map-entry-name: ${new_entry_name_1}, map-entry-str: ${new_entry_str_1}" + MAP "map-entry-name: ${new_entry_name_2}, map-entry-str: ${new_entry_str_2}" + ) + + yaml_save(NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create) + + # Read-back the yaml and verify the values. + yaml_load(FILE ${CMAKE_BINARY_DIR}/${CMAKE_CURRENT_FUNCTION}_test_create.yaml + NAME ${CMAKE_CURRENT_FUNCTION}_readback + ) + + yaml_length(readback NAME ${CMAKE_CURRENT_FUNCTION}_readback KEY cmake test set map-list) + + test_assert(TEST 3 EQUAL ${readback} + COMMENT "readback yaml list length does not match original." + ) + + foreach(index 0 1 2) + yaml_get(readback_name NAME ${CMAKE_CURRENT_FUNCTION}_readback KEY cmake test set map-list ${index} map-entry-name) + yaml_get(readback_str NAME ${CMAKE_CURRENT_FUNCTION}_readback KEY cmake test set map-list ${index} map-entry-str) + + test_assert(TEST "${readback_name}" STREQUAL "${new_entry_name_${index}}" + COMMENT "list values mismatch." + ) + test_assert(TEST "'${readback_str}'" STREQUAL "${new_entry_str_${index}}" + COMMENT "list values mismatch." + ) + endforeach() +endfunction() function(test_setting_empty_value) yaml_create(FILE ${CMAKE_BINARY_DIR}/${CMAKE_CURRENT_FUNCTION}_test_create.yaml NAME ${CMAKE_CURRENT_FUNCTION}_yaml-create @@ -558,9 +717,11 @@ test_reading_string() test_reading_int() test_reading_list_strings() test_reading_list_int() +test_reading_map_list_entry() test_reading_not_found() test_reading_not_found_array() test_reading_not_array() +test_reading_not_found_map_list_entry() test_save_new_file() @@ -568,6 +729,9 @@ test_setting_int() test_setting_string() test_setting_list_strings() test_setting_list_int() +test_setting_map_list_entry() +test_setting_map_list_entry_windows() +test_setting_map_list_entry_commas() test_setting_empty_value() test_setting_empty_list() diff --git a/tests/cmake/yaml/test.yaml b/tests/cmake/yaml/test.yaml index 4f21cff2c2a..f106a360ed8 100644 --- a/tests/cmake/yaml/test.yaml +++ b/tests/cmake/yaml/test.yaml @@ -11,3 +11,8 @@ cmake: - "list" - "of" - "strings" + map-list: + - map-entry-name: "MapEntry1" + map-entry-int: 5 + - map-entry-name: "MapEntry2" + map-entry-int: 4