go.ligato.io/vpp-agent/v3@v3.5.0/pkg/models/remote_model.go (about) 1 // Copyright (c) 2020 Pantheon.tech 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 models 16 17 import ( 18 "encoding/json" 19 "reflect" 20 "strings" 21 "unicode" 22 "unicode/utf8" 23 24 "github.com/go-errors/errors" 25 "go.ligato.io/cn-infra/v2/logging/logrus" 26 "google.golang.org/protobuf/encoding/protojson" 27 "google.golang.org/protobuf/proto" 28 "google.golang.org/protobuf/reflect/protoreflect" 29 "google.golang.org/protobuf/types/dynamicpb" 30 31 api "go.ligato.io/vpp-agent/v3/proto/ligato/generic" 32 ) 33 34 // RemotelyKnownModel represents a registered remote model (remote model has only information about model 35 // from remote source, i.e. missing go type because VPP-Agent meta service doesn't provide it) 36 type RemotelyKnownModel struct { 37 model *ModelInfo 38 } 39 40 // Spec returns model specification for the model. 41 func (m *RemotelyKnownModel) Spec() *Spec { 42 spec := ToSpec(m.model.Spec) 43 return &spec 44 } 45 46 // ModelDetail returns descriptor for the model. 47 func (m *RemotelyKnownModel) ModelDetail() *api.ModelDetail { 48 return m.model.ModelDetail 49 } 50 51 // NewInstance creates new instance value for model type. Due to missing go type for remote models, the created 52 // instance won't have the same go type as in case of local models, but dynamic proto message's go type 53 // (the proto descriptor will be the same). 54 func (m *RemotelyKnownModel) NewInstance() proto.Message { 55 return dynamicpb.NewMessageType(m.model.MessageDescriptor).New().Interface() 56 } 57 58 // ProtoName returns proto message name registered with the model. 59 func (m *RemotelyKnownModel) ProtoName() string { 60 if strings.TrimSpace(m.model.ProtoName) == "" { 61 return string(m.model.MessageDescriptor.FullName()) 62 } 63 return m.model.ProtoName 64 } 65 66 // ProtoFile returns proto file name for the model. 67 func (m *RemotelyKnownModel) ProtoFile() string { 68 return m.model.MessageDescriptor.ParentFile().Path() 69 } 70 71 // NameTemplate returns name template for the model. 72 func (m *RemotelyKnownModel) NameTemplate() string { 73 nameTemplate, _ := m.modelOptionFor("nameTemplate", m.model.Options) 74 return nameTemplate 75 } 76 77 // GoType returns go type for the model. 78 func (m *RemotelyKnownModel) GoType() string { 79 goType, _ := m.modelOptionFor("goType", m.model.Options) 80 return goType 81 } 82 83 // LocalGoType should returns reflect go type for the model, but remotely known model doesn't have 84 // locally known reflect go type. It always returns nil. 85 func (m *RemotelyKnownModel) LocalGoType() reflect.Type { 86 return nil 87 } 88 89 // PkgPath returns package import path for the model definition. 90 func (m *RemotelyKnownModel) PkgPath() string { 91 pkgPath, _ := m.modelOptionFor("pkgPath", m.model.Options) 92 return pkgPath 93 } 94 95 // Name returns name for the model. 96 func (m *RemotelyKnownModel) Name() string { 97 return ToSpec(m.model.Spec).ModelName() 98 } 99 100 // KeyPrefix returns key prefix for the model. 101 func (m *RemotelyKnownModel) KeyPrefix() string { 102 return keyPrefix(ToSpec(m.model.Spec), m.NameTemplate() != "") 103 } 104 105 // ParseKey parses the given key and returns item name 106 // or returns empty name and valid as false if the key is not valid. 107 func (m *RemotelyKnownModel) ParseKey(key string) (name string, valid bool) { 108 name = strings.TrimPrefix(key, m.KeyPrefix()) 109 if name == key || (name == "" && m.NameTemplate() != "") { 110 name = strings.TrimPrefix(key, m.Name()) 111 } 112 // key had the prefix and also either 113 // non-empty name or no name template 114 if name != key && (name != "" || m.NameTemplate() == "") { 115 // TODO: validate name? 116 return name, true 117 } 118 return "", false 119 } 120 121 // IsKeyValid returns true if given key is valid for this model. 122 func (m *RemotelyKnownModel) IsKeyValid(key string) bool { 123 _, valid := m.ParseKey(key) 124 return valid 125 } 126 127 // StripKeyPrefix returns key with prefix stripped. 128 func (m *RemotelyKnownModel) StripKeyPrefix(key string) string { 129 if name, valid := m.ParseKey(key); valid { 130 return name 131 } 132 return key 133 } 134 135 // InstanceName computes message name for given proto message using name template (if present). 136 func (m *RemotelyKnownModel) InstanceName(x interface{}) (string, error) { 137 message := protoMessageOf(x) 138 139 // extract data from message and use them with name template to get the name 140 nameTemplate, err := m.modelOptionFor("nameTemplate", m.model.Options) 141 if err != nil { 142 logrus.DefaultLogger().Debugf("no nameTemplate model "+ 143 "option for model %v, using empty instance name", m.model.ProtoName) 144 return "", nil // having no name template is valid case for some models 145 } 146 nameTemplate = m.replaceFieldNamesInNameTemplate(m.model.MessageDescriptor, nameTemplate) 147 marshaller := protojson.MarshalOptions{ 148 EmitUnpopulated: true, 149 } 150 jsonData, err := marshaller.Marshal(message) 151 if err != nil { 152 return "", errors.Errorf("can't marshall message "+ 153 "to json due to: %v (message: %+v)", err, message) 154 } 155 var mapData map[string]interface{} 156 if err := json.Unmarshal(jsonData, &mapData); err != nil { 157 return "", errors.Errorf("can't load json of marshalled "+ 158 "message to generic map due to: %v (json=%v)", err, jsonData) 159 } 160 name, err := NameTemplate(nameTemplate)(mapData) 161 if err != nil { 162 return "", errors.Errorf("can't compute name from name template by applying generic map "+ 163 "due to: %v (name template=%v, generic map=%v)", err, nameTemplate, mapData) 164 } 165 return name, nil 166 } 167 168 // replaceFieldNamesInNameTemplate replaces JSON field names to Go Type field name in name template. 169 func (m *RemotelyKnownModel) replaceFieldNamesInNameTemplate(messageDesc protoreflect.MessageDescriptor, nameTemplate string) string { 170 // FIXME this is only a good effort to map between NameTemplate variables and Proto model field names 171 // (protoName, jsonName). We can do here better (fix field names prefixing other field names or field 172 // names colliding with field names of inner reference structures), but i the end we are still guessing 173 // without knowledge of go type. Can we fix this? 174 // (The dynamicpb.NewMessageType(messageDesc) should return MessageType that joins message descriptor and 175 // go type information, but for dynamicpb package the go type means always dynamicpb.Message and not real 176 // go type of generated models. We could use some other MessageType implementation, but they always need 177 // the go type informations(reflect.Type) so without it the MessageType is useless for solving this) 178 for i := 0; i < messageDesc.Fields().Len(); i++ { 179 fieldDesc := messageDesc.Fields().Get(i) 180 pbJSONName := fieldDesc.JSONName() 181 nameTemplate = strings.ReplaceAll(nameTemplate, "."+m.upperFirst(pbJSONName), "."+pbJSONName) 182 if fieldDesc.Message() != nil { 183 nameTemplate = m.replaceFieldNamesInNameTemplate(fieldDesc.Message(), nameTemplate) 184 } 185 } 186 return nameTemplate 187 } 188 189 // upperFirst converts the first letter of string to upper case 190 func (m *RemotelyKnownModel) upperFirst(s string) string { 191 if s == "" { 192 return "" 193 } 194 r, n := utf8.DecodeRuneInString(s) 195 return string(unicode.ToUpper(r)) + s[n:] 196 } 197 198 // modelOptionFor retrieves first value for given key in model detail options 199 func (m *RemotelyKnownModel) modelOptionFor(key string, options []*api.ModelDetail_Option) (string, error) { 200 for _, option := range options { 201 if option.Key == key { 202 if len(option.Values) == 0 { 203 return "", errors.Errorf("there is no value for key %v in model options", key) 204 } 205 if strings.TrimSpace(option.Values[0]) == "" { 206 return "", errors.Errorf("there is no value(only empty string "+ 207 "after trimming) for key %v in model options", key) 208 } 209 return option.Values[0], nil 210 } 211 } 212 return "", errors.Errorf("there is no model option with key %v (model options=%+v))", key, options) 213 }