chromium/infra/config/lib/structs.star

# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Library for working with starlark structs.

The functionality for this module can be used by loading the structs symbol,
which provides the following functions:
* to_properties - convert a struct to a dict that can represent a proto in
    properties
* evolve - set new values for struct attributes
* extend - extend values for struct attributes
* remove - remove values for struct attributes
"""

load("./args.star", "args")

def _convert_to_dict(s):
    """Convert a struct to a dict

    Due to starlark not supporting recursion, in order to enable a struct
    containing nested structs to be recursively converted, the conversion won't
    necessarily be complete so a partially converted dict will be returned along
    with information of the attributes that need to themselves be converted.

    Args:
        s: The struct to convert.

    Returns:
        A 2-tuple:
            * A dict of converted attributes.
            * A sequence of 3-tuples for each attribute that needs to be recursively
            converted:
                * The dictionary to add the converted value to.
                * The name of the key to set with the converted value.
                * The value to convert.
    """
    d = {}
    to_convert = []
    for a in dir(s):
        v = getattr(s, a)
        if v == None or v == []:
            continue
        if type(v) == type(struct()):
            to_convert.append((d, a, v))
            continue
        d[a] = v
    return d, to_convert

def _to_proto_properties(s):
    """Converts a struct to a properties dict that can represent a proto.

    Args:
        s: The struct to convert.

    Returns:
        A dict with items corresponding to the attributes of `s`. Attributes
        with a None value or empty list will be omitted from the dict since the
        corresponding jsonpb values are equivalent to the field not being set.
    """
    top_level_dict, to_convert = _convert_to_dict(s)

    # Since starlark doesn't support recursion, iterate over the depth of the
    # structure, recording the work to be done at the next depth. Since starlark
    # doesn't support while loops, iterate up to an aribtrary maximum depth and
    # break out of the loop if there's no more work to be done.
    for _ in range(15):
        if not to_convert:
            break
        next_to_convert = []
        for d, k, s in to_convert:
            converted_s, to_convert_for_s = _convert_to_dict(s)
            next_to_convert.extend(to_convert_for_s)
            d[k] = converted_s
        to_convert = next_to_convert

    if to_convert:
        fail("excessively nested struct")

    return top_level_dict

def _evolve(s, **kwargs):
    """Modify a struct's attributes.

    Args:
        s: The struct to modify.
        **kwargs: The attributes to update the struct with.

    Returns:
        A new struct with the value of each attribute specified in `kwargs`
        is set to the corresponding value.

    Fails:
        If `kwargs` contains an item for an attribute not present on `s`.
    """
    d = {a: getattr(s, a) for a in dir(s)}
    for k, v in kwargs.items():
        if k not in d:
            fail("attempting to modify unknown field {!r}".format(k))
        d[k] = v
    return struct(**d)

def _extend(s, **kwargs):
    """Extend a struct's list attributes.

    Args:
        s: The struct to modify.
        **kwargs: The attributes to update the struct with. The value of each
            element can either be a single value or a list of values.

    Returns:
        A new struct where the value of each attribute specified in `kwargs`
        is set to the combination of the existing list of values if any and the
        values specified for the attribute in `kwargs`.

    Fails:
        If `kwargs` contains an item for an attribute not present on `s`.
        If any of the attributes to modify do not have list values.
    """
    d = {a: getattr(s, a) for a in dir(s)}
    for k, v in kwargs.items():
        if k not in d:
            fail("attempting to modify unknown field {!r}".format(k))
        value = d[k]
        if type(value) != type([]):
            fail("attempting to extend a non-list field {!r}".format(k))
        d[k] = args.listify(value, v)
    return struct(**d)

def _remove(s, **kwargs):
    """Remove elements from a struct's list attributes.

    Args:
        s: The struct to modify.
        **kwargs: The attributes to update the struct with. The value of each
            element can either be a single value or a list of values.

    Returns:
        A new struct where the value of each attribute specified in `kwargs`
        has the given values removed.

    Fails:
        If `kwargs` contains an item for an attribute not present on `s`.
        If any of the attributes to modify do not have list values.
        If any of the elements to remove do not exist in the specified
            attribute.
    """
    d = {a: getattr(s, a) for a in dir(s)}
    for k, v in kwargs.items():
        if k not in d:
            fail("attempting to modify unknown field {!r}".format(k))
        value = d[k]
        if type(value) != type([]):
            fail("attempting to remove elements from a non-list field {!r}"
                .format(k))
        for e in args.listify(v):
            value.remove(e)
        d[k] = value
    return struct(**d)

structs = struct(
    to_proto_properties = _to_proto_properties,
    evolve = _evolve,
    extend = _extend,
    remove = _remove,
)