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  }