go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/starlark/starlarkproto/type.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package starlarkproto
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  
    21  	"go.starlark.net/starlark"
    22  
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/reflect/protoreflect"
    25  
    26  	"go.chromium.org/luci/starlark/typed"
    27  )
    28  
    29  // MessageType represents a proto message type and acts as its constructor: it
    30  // is a Starlark callable that produces instances of Message.
    31  //
    32  // Implements starlark.HasAttrs interface. Attributes represent constructors for
    33  // nested messages and int values of enums. Note that starlark.HasSetField is
    34  // not implemented, making values of MessageType immutable.
    35  //
    36  // Stringifying an instance of MessageType (e.g. with `str(...)` in Starlark)
    37  // returns its full proto type name.
    38  //
    39  // Given a MessageDescriptor, MessageType can be instantiated through Loader as
    40  // loader.MessageType(...).
    41  type MessageType struct {
    42  	*starlark.Builtin // the callable, initialize in Loader
    43  
    44  	loader *Loader                                 // owning Loader
    45  	desc   protoreflect.MessageDescriptor          // original descriptor
    46  	attrs  starlark.StringDict                     // nested symbols, e.g. submessages and enums
    47  	fields map[string]protoreflect.FieldDescriptor // message fields (including oneof alternatives)
    48  	keys   []string                                // sorted keys of 'fields' map
    49  }
    50  
    51  // initLocked preprocesses message descriptor.
    52  //
    53  // Called under the loader lock.
    54  func (t *MessageType) initLocked() {
    55  	fields := t.desc.Fields() // note: this already includes oneof alternatives
    56  	t.fields = make(map[string]protoreflect.FieldDescriptor, fields.Len())
    57  	t.keys = make([]string, fields.Len())
    58  	for i := 0; i < fields.Len(); i++ {
    59  		fd := fields.Get(i)
    60  		key := string(fd.Name())
    61  		t.fields[key] = fd
    62  		t.keys[i] = key
    63  	}
    64  }
    65  
    66  // Descriptor returns protobuf type information for this message type.
    67  func (t *MessageType) Descriptor() protoreflect.MessageDescriptor {
    68  	return t.desc
    69  }
    70  
    71  // Loader returns the loader this message type was loaded from.
    72  func (t *MessageType) Loader() *Loader {
    73  	return t.loader
    74  }
    75  
    76  // Message instantiates a new empty message of this type.
    77  func (t *MessageType) Message() *Message {
    78  	return &Message{
    79  		typ:    t,
    80  		fields: starlark.StringDict{},
    81  	}
    82  }
    83  
    84  // MessageFromProto instantiates a new message of this type and populates it
    85  // based on values in the given proto.Message that should have a matching type.
    86  //
    87  // Here "matching type" means p.ProtoReflect().Descriptor() *is* t.Descriptor().
    88  // Panics otherwise.
    89  func (t *MessageType) MessageFromProto(p proto.Message) *Message {
    90  	refl := p.ProtoReflect()
    91  	if got := refl.Descriptor(); got != t.desc {
    92  		panic(fmt.Errorf("bad message type: got %s, want %s", got.FullName(), t.desc.FullName()))
    93  	}
    94  	m := t.Message()
    95  	refl.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
    96  		err := m.SetField(string(fd.Name()), toStarlark(t.loader, fd, v))
    97  		if err != nil {
    98  			panic(fmt.Errorf("internal error: field %q: %s", fd.Name(), err))
    99  		}
   100  		return true
   101  	})
   102  	return m
   103  }
   104  
   105  // Converter returns an object that can convert Starlark dicts and Nones to
   106  // values of this message type.
   107  //
   108  // Can be used by typed.List and typed.Dict.
   109  func (t *MessageType) Converter() typed.Converter {
   110  	// Note: it is important that t.Converter() == t.Converter() at all times.
   111  	// Converters are compared by identity when checking type matches.
   112  	return messageConverter{t}
   113  }
   114  
   115  // Starlark.Value interface.
   116  
   117  // Type returns "proto.MessageType", it's the type of the message type itself.
   118  //
   119  // It is sort of like a meta-type, i.e. like "type" type in Python.
   120  func (t *MessageType) Type() string {
   121  	return "proto.MessageType"
   122  }
   123  
   124  // String returns the full proto type name.
   125  func (t *MessageType) String() string {
   126  	return string(t.desc.FullName())
   127  }
   128  
   129  // Attr returns either a nested message or an enum value.
   130  func (t *MessageType) Attr(name string) (starlark.Value, error) {
   131  	return t.attrs[name], nil // (nil, nil) means "not found"
   132  }
   133  
   134  // AttrNames return names of all nested messages and enum values.
   135  func (t *MessageType) AttrNames() []string {
   136  	keys := make([]string, 0, len(t.attrs))
   137  	for k := range t.attrs {
   138  		keys = append(keys, k)
   139  	}
   140  	sort.Strings(keys)
   141  	return keys
   142  }
   143  
   144  // typed.Converter implementation.
   145  
   146  type messageConverter struct {
   147  	typ *MessageType
   148  }
   149  
   150  // Convert returns 'x' as is if it already has type 'c.typ', otherwise it
   151  // initializes a new message of type 'c.typ' from 'x'.
   152  //
   153  // 'x' can be either None (in which case an empty message is initialized) or an
   154  // iterable mapping (e.g. a dict).
   155  func (c messageConverter) Convert(x starlark.Value) (starlark.Value, error) {
   156  	if msg, ok := x.(*Message); ok {
   157  		if msg.typ == c.typ {
   158  			return msg, nil
   159  		}
   160  		return nil, fmt.Errorf("got %s, want %s", msg.Type(), c.Type())
   161  	}
   162  
   163  	if x == starlark.None {
   164  		return c.typ.Message(), nil
   165  	}
   166  
   167  	if d, ok := x.(starlark.IterableMapping); ok {
   168  		m := c.typ.Message()
   169  		if err := m.FromDict(d); err != nil {
   170  			return nil, err
   171  		}
   172  		return m, nil
   173  	}
   174  
   175  	return nil, fmt.Errorf("got %s, want %s", x.Type(), c.Type())
   176  }
   177  
   178  // Type returns name of the type 'c' converts to.
   179  func (c messageConverter) Type() string {
   180  	return fmt.Sprintf("proto.Message<%s>", c.typ.desc.FullName())
   181  }