cmake: support array of maps in yaml module

This commit introduce support for maps in a yaml list.

The yaml_set() function has been extended with the following signature:
> yaml_set(NAME <name> KEY <key>...
>          [APPEND] LIST MAP <map1> MAP <map2> MAP ...
> )

where a `MAP <map>` has the form:
`MAP "<key1>: <value1>, <key2>: <value2>, ...`

Signed-off-by: Torsten Rasmussen <Torsten.Rasmussen@nordicsemi.no>
This commit is contained in:
Torsten Rasmussen 2025-01-10 13:33:08 +01:00 committed by Fabio Baltieri
commit 0828d0b0ef
3 changed files with 250 additions and 15 deletions

View file

@ -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 <name> KEY <key>... [GENEX] VALUE <value>)
# yaml_set(NAME <name> KEY <key>... [APPEND] [GENEX] LIST <value>...)
# yaml_set(NAME <name> KEY <key>... [APPEND] LIST MAP <map1> MAP <map2> 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> : Map, with key-value pairs where key-value is separated by ':',
# and pairs separated by ','.
# Format example: "<key1>: <value1>, <key2>: <value2>, ..."
# 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)

View file

@ -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()

View file

@ -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