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 }