1# SPDX-License-Identifier: Apache-2.0
2#
3# Copyright (c) 2024, Nordic Semiconductor ASA
4
5# CMake YAML module for handling of YAML files.
6#
7# This module offers basic support for simple yaml files.
8#
9# It supports basic key-value pairs, like
10# foo: bar
11#
12# basic key-object pairs, like
13# foo:
14#    bar: baz
15#
16# Simple value lists, like:
17# foos:
18#  - foo1
19#  - foo2
20#  - foo3
21#
22# Support for list of maps, like:
23# foo:
24#  - bar: val1
25#    baz: val1
26#  - bar: val2
27#    baz: val2
28#
29# All of above can be combined, for example like:
30# foo:
31#   bar: baz
32#   quz:
33#     greek:
34#      - alpha
35#      - beta
36#      - gamma
37# fred: thud
38
39include_guard(GLOBAL)
40
41include(extensions)
42include(python)
43
44# Internal helper function for checking that a YAML context has been created
45# before operating on it.
46# Will result in CMake error if context does not exist.
47function(internal_yaml_context_required)
48  cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN})
49  zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME)
50  yaml_context(EXISTS NAME ${ARG_YAML_NAME} result)
51
52  if(NOT result)
53    message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' does not exist."
54            "Remember to create a YAML context using 'yaml_create()' or 'yaml_load()'"
55    )
56  endif()
57endfunction()
58
59# Internal helper function for checking if a YAML context is free before creating
60# it later.
61# Will result in CMake error if context exists.
62function(internal_yaml_context_free)
63  cmake_parse_arguments(ARG_YAML "" "NAME" "" ${ARGN})
64  zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME)
65  yaml_context(EXISTS NAME ${ARG_YAML_NAME} result)
66
67  if(result)
68    message(FATAL_ERROR "YAML context '${ARG_YAML_NAME}' already exists."
69            "Please create a YAML context with a unique name"
70    )
71  endif()
72endfunction()
73
74# Internal helper function to provide the correct initializer for a list in the
75# JSON content.
76function(internal_yaml_list_initializer var genex)
77  if(genex)
78    set(${var} "\"@YAML-LIST@\"" PARENT_SCOPE)
79  else()
80    set(${var} "[]" PARENT_SCOPE)
81  endif()
82endfunction()
83
84# Internal helper function to append items to a list in the JSON content.
85# Unassigned arguments are the values to be appended.
86function(internal_yaml_list_append var genex key)
87  set(json_content "${${var}}")
88  string(JSON subjson GET "${json_content}" ${key})
89  if(genex)
90    # new lists are stored in CMake string format, but those imported via
91    # yaml_load() are proper JSON arrays. When an append is requested, those
92    # must be converted back to a CMake list.
93    string(JSON type TYPE "${json_content}" ${key})
94    if(type STREQUAL ARRAY)
95      string(JSON arraylength LENGTH "${subjson}")
96      internal_yaml_list_initializer(subjson TRUE)
97      if(${arraylength} GREATER 0)
98        math(EXPR arraystop "${arraylength} - 1")
99        list(GET ARG_YAML_LIST 0 entry_0)
100        if(entry_0 STREQUAL MAP)
101          message(FATAL_ERROR "${function}(GENEX ${argument} ) is not valid at this position.\n"
102                    "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\""
103            )
104        endif()
105
106        foreach(i RANGE 0 ${arraystop})
107          string(JSON item GET "${json_content}" ${key} ${i})
108          list(APPEND subjson ${item})
109        endforeach()
110      endif()
111    endif()
112    list(APPEND subjson ${ARGN})
113    string(JSON json_content SET "${json_content}" ${key} "\"${subjson}\"")
114  else()
115    # lists are stored as JSON arrays
116    string(JSON index LENGTH "${subjson}")
117    list(LENGTH ARGN length)
118    if(NOT length EQUAL 0)
119      list(GET ARG_YAML_LIST 0 entry_0)
120      if(entry_0 STREQUAL MAP)
121        math(EXPR length "${length} / 2")
122        math(EXPR stop "${index} + ${length} - 1")
123        foreach(i RANGE ${index} ${stop})
124          list(POP_FRONT ARG_YAML_LIST argument)
125          if(NOT argument STREQUAL MAP)
126            message(FATAL_ERROR "yaml_set(${argument} ) is not valid at this position.\n"
127                    "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\""
128            )
129          endif()
130          list(POP_FRONT ARG_YAML_LIST map_value)
131          string(REGEX REPLACE "([^\\])," "\\1;" pair_list "${map_value}")
132          set(qouted_map_value)
133          foreach(pair ${pair_list})
134            if(NOT pair MATCHES "[^ ]*:[^ ]*")
135              message(FATAL_ERROR "yaml_set(MAP ${map_value} ) is malformed.\n"
136                    "Syntax is 'LIST MAP \"key1: value1.1, ...\" MAP \"key1: value1.2, ...\"\n"
137                    "If value contains comma ',' then ensure the value field is properly qouted "
138                    "and escaped"
139              )
140            endif()
141            string(REGEX MATCH "^[^:]*" map_key "${pair}")
142            string(REGEX REPLACE "^${map_key}:[ ]*" "" value "${pair}")
143            string(STRIP "${map_key}" map_key)
144            if(value MATCHES "," AND NOT (value MATCHES "\\\\," AND value MATCHES "'.*'"))
145              message(FATAL_ERROR "value: ${value} is not properly quoted")
146            endif()
147            string(REGEX REPLACE "\\\\," "," value "${value}")
148            list(APPEND qouted_map_value "\"${map_key}\": \"${value}\"")
149          endforeach()
150          list(JOIN qouted_map_value "," qouted_map_value)
151          string(JSON json_content SET "${json_content}" ${key} ${i} "{${qouted_map_value}}")
152        endforeach()
153      else()
154        math(EXPR stop "${index} + ${length} - 1")
155        list(GET ARG_YAML_LIST 0 entry_0)
156          foreach(i RANGE ${index} ${stop})
157            list(POP_FRONT ARGN value)
158            string(JSON json_content SET "${json_content}" ${key} ${i} "\"${value}\"")
159          endforeach()
160      endif()
161    endif()
162  endif()
163  set(${var} "${json_content}" PARENT_SCOPE)
164endfunction()
165
166# Usage
167#   yaml_context(EXISTS NAME <name> <result>)
168#
169# Function to query the status of the YAML context with the name <name>.
170# The result of the query is stored in <result>
171#
172# EXISTS     : Check if the YAML context exists in the current scope
173#              If the context exists, then TRUE is returned in <result>
174# NAME <name>: Name of the YAML context
175# <result>   : Variable to store the result of the query.
176#
177function(yaml_context)
178  cmake_parse_arguments(ARG_YAML "EXISTS" "NAME" "" ${ARGN})
179  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML EXISTS NAME)
180
181  if(NOT DEFINED ARG_YAML_UNPARSED_ARGUMENTS)
182    message(FATAL_ERROR "Missing argument in "
183            "${CMAKE_CURRENT_FUNCTION}(EXISTS NAME ${ARG_YAML_NAME} <result-var>)."
184    )
185  endif()
186
187  zephyr_scope_exists(scope_defined ${ARG_YAML_NAME})
188  if(scope_defined)
189    list(POP_FRONT ARG_YAML_UNPARSED_ARGUMENTS out-var)
190    set(${out-var} TRUE PARENT_SCOPE)
191  else()
192    set(${out-var} ${ARG_YAML_NAME}-NOTFOUND PARENT_SCOPE)
193  endif()
194endfunction()
195
196# Usage:
197#   yaml_create(NAME <name> [FILE <file>])
198#
199# Create a new empty YAML context.
200# Use the file <file> for storing the context when 'yaml_save(NAME <name>)' is
201# called.
202#
203# Values can be set by calling 'yaml_set(NAME <name>)' by using the <name>
204# specified when creating the YAML context.
205#
206# NAME <name>: Name of the YAML context.
207# FILE <file>: Path to file to be used together with this YAML context.
208#
209function(yaml_create)
210  cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN})
211
212  zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME)
213
214  internal_yaml_context_free(NAME ${ARG_YAML_NAME})
215  zephyr_create_scope(${ARG_YAML_NAME})
216  if(DEFINED ARG_YAML_FILE)
217    zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME})
218  endif()
219  zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME})
220  zephyr_set(JSON "{}" SCOPE ${ARG_YAML_NAME})
221endfunction()
222
223# Usage:
224#   yaml_load(FILE <file> NAME <name>)
225#
226# Load an existing YAML file and store its content in the YAML context <name>.
227#
228# Values can later be retrieved ('yaml_get()') or set/updated ('yaml_set()') by using
229# the same YAML scope name.
230#
231# FILE <file>: Path to file to load.
232# NAME <name>: Name of the YAML context.
233#
234function(yaml_load)
235  cmake_parse_arguments(ARG_YAML "" "FILE;NAME" "" ${ARGN})
236
237  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE NAME)
238  internal_yaml_context_free(NAME ${ARG_YAML_NAME})
239
240  zephyr_create_scope(${ARG_YAML_NAME})
241  zephyr_set(FILE ${ARG_YAML_FILE} SCOPE ${ARG_YAML_NAME})
242
243  execute_process(COMMAND ${PYTHON_EXECUTABLE} -c
244    "import json; import yaml; print(json.dumps(yaml.safe_load(open('${ARG_YAML_FILE}')) or {}))"
245    OUTPUT_VARIABLE json_load_out
246    ERROR_VARIABLE json_load_error
247    RESULT_VARIABLE json_load_result
248  )
249
250  if(json_load_result)
251    message(FATAL_ERROR "Failed to load content of YAML file: ${ARG_YAML_FILE}\n"
252                        "${json_load_error}"
253    )
254  endif()
255
256  zephyr_set(GENEX FALSE SCOPE ${ARG_YAML_NAME})
257  zephyr_set(JSON "${json_load_out}" SCOPE ${ARG_YAML_NAME})
258endfunction()
259
260# Usage:
261#   yaml_get(<out-var> NAME <name> KEY <key>...)
262#
263# Get the value of the given key and store the value in <out-var>.
264# If key represents a list, then the list is returned.
265#
266# Behavior is undefined if key points to a complex object.
267#
268# NAME <name>  : Name of the YAML context.
269# KEY <key>... : Name of key.
270# <out-var>    : Name of output variable.
271#
272function(yaml_get out_var)
273  # Current limitation:
274  # - Anything will be returned, even json object strings.
275  cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN})
276
277  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY)
278  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
279
280  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
281
282  # We specify error variable to avoid a fatal error.
283  # If key is not found, then type becomes '-NOTFOUND' and value handling is done below.
284  string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY})
285  if(type STREQUAL ARRAY)
286    string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY})
287    string(JSON arraylength LENGTH "${subjson}")
288    set(array)
289    math(EXPR arraystop "${arraylength} - 1")
290    if(arraylength GREATER 0)
291      foreach(i RANGE 0 ${arraystop})
292        string(JSON item GET "${subjson}" ${i})
293        list(APPEND array ${item})
294      endforeach()
295    endif()
296    set(${out_var} ${array} PARENT_SCOPE)
297  else()
298    # We specify error variable to avoid a fatal error.
299    # Searching for a non-existing key should just result in the output value '-NOTFOUND'
300    string(JSON value ERROR_VARIABLE error GET "${json_content}" ${ARG_YAML_KEY})
301    set(${out_var} ${value} PARENT_SCOPE)
302  endif()
303endfunction()
304
305# Usage:
306#   yaml_length(<out-var> NAME <name> KEY <key>...)
307#
308# Get the length of the array defined by the given key and store the length in <out-var>.
309# If key does not define an array, then the length -1 is returned.
310#
311# NAME <name>  : Name of the YAML context.
312# KEY <key>... : Name of key defining the list.
313# <out-var>    : Name of output variable.
314#
315function(yaml_length out_var)
316  cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN})
317
318  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY)
319  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
320
321  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
322
323  string(JSON type ERROR_VARIABLE error TYPE "${json_content}" ${ARG_YAML_KEY})
324  if(type STREQUAL ARRAY)
325    string(JSON subjson GET "${json_content}" ${ARG_YAML_KEY})
326    string(JSON arraylength LENGTH "${subjson}")
327    set(${out_var} ${arraylength} PARENT_SCOPE)
328  elseif(type MATCHES ".*-NOTFOUND")
329    set(${out_var} ${type} PARENT_SCOPE)
330  else()
331    message(WARNING "YAML key: ${ARG_YAML_KEY} is not an array.")
332    set(${out_var} -1 PARENT_SCOPE)
333  endif()
334endfunction()
335
336# Usage:
337#   yaml_set(NAME <name> KEY <key>... [GENEX] VALUE <value>)
338#   yaml_set(NAME <name> KEY <key>... [APPEND] [GENEX] LIST <value>...)
339#   yaml_set(NAME <name> KEY <key>... [APPEND] LIST MAP <map1> MAP <map2> MAP ...)
340#
341# Set a value or a list of values to given key.
342#
343# If setting a list of values, then APPEND can be specified to indicate that the
344# list of values should be appended to the existing list identified with key(s).
345#
346# NAME <name>  : Name of the YAML context.
347# KEY <key>... : Name of key.
348# VALUE <value>: New value for the key.
349# LIST <values>: New list of values for the key.
350# APPEND       : Append the list of values to the list of values for the key.
351# GENEX        : The value(s) contain generator expressions. When using this
352#                option, also see the notes in the yaml_save() function.
353# MAP <map>    : Map, with key-value pairs where key-value is separated by ':',
354#                and pairs separated by ','.
355#                Format example: "<key1>: <value1>, <key2>: <value2>, ..."
356#                MAP can be given multiple times to separate maps when adding them to a list.
357#                LIST MAP cannot be used with GENEX.
358#
359#                Note: if a map value contains commas, ',', then the value string must be quoted in
360#                      single quotes and commas must be double escaped, like this: 'A \\,string'
361#
362function(yaml_set)
363  cmake_parse_arguments(ARG_YAML "APPEND;GENEX" "NAME;VALUE" "KEY;LIST" ${ARGN})
364
365  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY)
366  zephyr_check_arguments_required_allow_empty(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST)
367  zephyr_check_arguments_exclusive(${CMAKE_CURRENT_FUNCTION} ARG_YAML VALUE LIST)
368  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
369
370  if(ARG_YAML_GENEX)
371    zephyr_set(GENEX TRUE SCOPE ${ARG_YAML_NAME})
372  endif()
373
374  if(DEFINED ARG_YAML_LIST
375     OR LIST IN_LIST ARG_YAML_KEYWORDS_MISSING_VALUES)
376    set(key_is_list TRUE)
377  endif()
378
379  if(ARG_YAML_APPEND AND NOT key_is_list)
380    message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION}(APPEND ...) can only be used with argument: LIST")
381  endif()
382
383  if(ARG_YAML_GENEX AND MAP IN_LIST ARG_YAML_LIST)
384    message(FATAL_ERROR "${function}(GENEX ...) cannot be used with argument: LIST MAP")
385  endif()
386
387  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
388  zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX)
389
390  set(yaml_key_undefined ${ARG_YAML_KEY})
391  foreach(k ${yaml_key_undefined})
392    list(REMOVE_AT yaml_key_undefined 0)
393    # We ignore any errors as we are checking for existence of the key, and
394    # non-existing keys will throw errors but also set type to NOT-FOUND.
395    string(JSON type ERROR_VARIABLE ignore TYPE "${json_content}" ${valid_keys} ${k})
396
397    if(NOT type)
398      list(APPEND yaml_key_create ${k})
399      break()
400    endif()
401    list(APPEND valid_keys ${k})
402  endforeach()
403
404  list(REVERSE yaml_key_undefined)
405  if(NOT "${yaml_key_undefined}" STREQUAL "")
406    if(key_is_list)
407      internal_yaml_list_initializer(json_string ${genex})
408    else()
409      set(json_string "\"\"")
410    endif()
411
412    foreach(k ${yaml_key_undefined})
413      set(json_string "{\"${k}\": ${json_string}}")
414    endforeach()
415    string(JSON json_content SET "${json_content}"
416           ${valid_keys} ${yaml_key_create} "${json_string}"
417    )
418  endif()
419
420  if(key_is_list)
421    if(NOT ARG_YAML_APPEND)
422      internal_yaml_list_initializer(json_string ${genex})
423      string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "${json_string}")
424    endif()
425    zephyr_string(ESCAPE escape_list "${ARG_YAML_LIST}")
426    internal_yaml_list_append(json_content ${genex} "${ARG_YAML_KEY}" ${escape_list})
427  else()
428    zephyr_string(ESCAPE escape_value "${ARG_YAML_VALUE}")
429    string(JSON json_content SET "${json_content}" ${ARG_YAML_KEY} "\"${escape_value}\"")
430  endif()
431
432  zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME})
433endfunction()
434
435# Usage:
436#   yaml_remove(NAME <name> KEY <key>...)
437#
438# Remove the KEY <key>... from the YAML context <name>.
439#
440# Several levels of keys can be given, for example:
441# KEY build cmake command
442#
443# To remove the key 'command' underneath 'cmake' in the toplevel 'build'
444#
445# NAME <name>: Name of the YAML context.
446# KEY <key>  : Name of key to remove.
447#
448function(yaml_remove)
449  cmake_parse_arguments(ARG_YAML "" "NAME" "KEY" ${ARGN})
450
451  zephyr_check_arguments_required_all(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME KEY)
452  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
453
454  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
455  string(JSON json_content REMOVE "${json_content}" ${ARG_YAML_KEY})
456
457  zephyr_set(JSON "${json_content}" SCOPE ${ARG_YAML_NAME})
458endfunction()
459
460# Usage:
461#   yaml_save(NAME <name> [FILE <file>])
462#
463# Write the YAML context <name> to <file>, or the one given with the earlier
464# 'yaml_load()' or 'yaml_create()' call. This will be performed immediately if
465# the context does not use generator expressions; otherwise, keys that include
466# a generator expression will initially be written as comments, and the full
467# contents will be available at build time. Build steps that depend on the file
468# being complete must depend on the '<name>_yaml_saved' target.
469#
470# NAME <name>: Name of the YAML context
471# FILE <file>: Path to file to write the context.
472#              If not given, then the FILE property of the YAML context will be
473#              used. In case both FILE is omitted and FILE property is missing
474#              on the YAML context, then an error will be raised.
475#
476function(yaml_save)
477  cmake_parse_arguments(ARG_YAML "" "NAME;FILE" "" ${ARGN})
478
479  zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML NAME)
480  internal_yaml_context_required(NAME ${ARG_YAML_NAME})
481
482  zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE)
483  if(NOT yaml_file)
484    zephyr_check_arguments_required(${CMAKE_CURRENT_FUNCTION} ARG_YAML FILE)
485  endif()
486  if(DEFINED ARG_YAML_FILE)
487    set(yaml_file ${ARG_YAML_FILE})
488  else()
489    zephyr_get_scoped(yaml_file ${ARG_YAML_NAME} FILE)
490  endif()
491
492  zephyr_get_scoped(genex ${ARG_YAML_NAME} GENEX)
493  zephyr_get_scoped(json_content ${ARG_YAML_NAME} JSON)
494  to_yaml("${json_content}" 0 yaml_out ${genex})
495
496  if(EXISTS ${yaml_file})
497    FILE(RENAME ${yaml_file} ${yaml_file}.bak)
498  endif()
499  FILE(WRITE ${yaml_file} "${yaml_out}")
500
501  set(save_target ${ARG_YAML_NAME}_yaml_saved)
502  if (NOT TARGET ${save_target})
503    # Create a target for the completion of the YAML save operation.
504    # This will be a dummy unless genexes are used.
505    add_custom_target(${save_target} ALL DEPENDS ${yaml_file})
506    set_target_properties(${save_target} PROPERTIES
507      genex_save_count 0
508      temp_files ""
509    )
510  endif()
511
512  if (genex)
513    get_property(genex_save_count TARGET ${save_target} PROPERTY genex_save_count)
514    if (${genex_save_count} EQUAL 0)
515      # First yaml_save() for this context with genexes enabled
516      add_custom_command(
517        OUTPUT ${yaml_file}
518        DEPENDS $<TARGET_PROPERTY:${save_target},json_file>
519        COMMAND ${CMAKE_COMMAND}
520                -DJSON_FILE="$<TARGET_PROPERTY:${save_target},json_file>"
521                -DYAML_FILE="${yaml_file}"
522                -DTEMP_FILES="$<TARGET_PROPERTY:${save_target},temp_files>"
523                -P ${ZEPHYR_BASE}/cmake/yaml-filter.cmake
524      )
525    endif()
526
527    math(EXPR genex_save_count "${genex_save_count} + 1")
528    set_property(TARGET ${save_target} PROPERTY genex_save_count ${genex_save_count})
529
530    cmake_path(SET yaml_path "${yaml_file}")
531    cmake_path(GET yaml_path STEM yaml_file_no_ext)
532    set(json_file ${yaml_file_no_ext}_${genex_save_count}.json)
533    set_property(TARGET ${save_target} PROPERTY json_file ${json_file})
534
535    # comment this to keep the temporary JSON files
536    set_property(TARGET ${save_target} APPEND PROPERTY temp_files ${json_file})
537
538    FILE(GENERATE OUTPUT ${json_file}
539      CONTENT "${json_content}"
540    )
541  endif()
542endfunction()
543
544function(to_yaml in_json level yaml genex)
545  zephyr_string(ESCAPE json "${in_json}")
546  if(level GREATER 0)
547    math(EXPR level_dec "${level} - 1")
548    set(indent_${level} "${indent_${level_dec}}  ")
549  endif()
550
551  string(JSON length LENGTH "${json}")
552  if(length EQUAL 0)
553    # Empty object
554    return()
555  endif()
556
557  math(EXPR stop "${length} - 1")
558  foreach(i RANGE 0 ${stop})
559    string(JSON member MEMBER "${json}" ${i})
560
561    string(JSON type TYPE "${json}" ${member})
562    string(JSON subjson GET "${json}" ${member})
563    if(type STREQUAL OBJECT)
564      # JSON object -> YAML dictionary
565      set(${yaml} "${${yaml}}${indent_${level}}${member}:\n")
566      math(EXPR sublevel "${level} + 1")
567      to_yaml("${subjson}" ${sublevel} ${yaml} ${genex})
568    elseif(type STREQUAL ARRAY)
569      # JSON array -> YAML list
570      set(${yaml} "${${yaml}}${indent_${level}}${member}:")
571      string(JSON arraylength LENGTH "${subjson}")
572      if(${arraylength} LESS 1)
573        set(${yaml} "${${yaml}} []\n")
574      else()
575        set(${yaml} "${${yaml}}\n")
576        math(EXPR arraystop "${arraylength} - 1")
577        foreach(i RANGE 0 ${arraystop})
578          string(JSON item GET "${json}" ${member} ${i})
579          # Check the length of item. Only OBJECT and ARRAY may have length, so a length at this
580          # level means `to_yaml()` should be called recursively.
581          string(JSON length ERROR_VARIABLE ignore LENGTH "${item}")
582          if(length)
583            set(non_indent_yaml)
584            to_yaml("${item}" 0 non_indent_yaml FALSE)
585            string(REGEX REPLACE "\n$" "" non_indent_yaml "${non_indent_yaml}")
586            string(REPLACE "\n" "\n${indent_${level}}   " indent_yaml "${non_indent_yaml}")
587            set(${yaml} "${${yaml}}${indent_${level}} - ${indent_yaml}\n")
588          else()
589            set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n")
590          endif()
591        endforeach()
592      endif()
593    elseif(type STREQUAL STRING)
594      # JSON string maps to multiple YAML types:
595      # - with unexpanded generator expressions: save as YAML comment
596      # - if it matches the special prefix: convert to YAML list
597      # - otherwise: save as YAML scalar
598      if (subjson MATCHES "\\$<.*>" AND ${genex})
599        # Yet unexpanded generator expression: save as comment
600        string(SUBSTRING ${indent_${level}} 1 -1 short_indent)
601        set(${yaml} "${${yaml}}#${short_indent}${member}: ${subjson}\n")
602      elseif(subjson MATCHES "^@YAML-LIST@")
603        # List-as-string: convert to list
604        set(${yaml} "${${yaml}}${indent_${level}}${member}:")
605        list(POP_FRONT subjson)
606        if(subjson STREQUAL "")
607          set(${yaml} "${${yaml}} []\n")
608        else()
609          set(${yaml} "${${yaml}}\n")
610          foreach(item ${subjson})
611            set(${yaml} "${${yaml}}${indent_${level}} - ${item}\n")
612          endforeach()
613        endif()
614      else()
615        # Raw strings: save as is
616        set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n")
617      endif()
618    else()
619      # Other JSON data type -> YAML scalar, as-is
620      set(${yaml} "${${yaml}}${indent_${level}}${member}: ${subjson}\n")
621    endif()
622  endforeach()
623
624  set(${yaml} ${${yaml}} PARENT_SCOPE)
625endfunction()
626