| # 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, |
| ) |