// Package qsl creates QMessages compliant with the QMP specification. package qsl import ( "encoding/json" "errors" "io/ioutil" "strconv" "sync" "gitlab-app.eng.qops.net/golang/qmp/qsl/beacon" "gitlab-app.eng.qops.net/golang/qmp/qsl/internal/qm" "gitlab-app.eng.qops.net/golang/qmp/qsl/internal/schema" ) // Instance is the singleton Manager. var Instance Manager var once sync.Once // ErrNeedRegister indicates a NewReader or NewWriter without RegisterApplication. var ErrNeedRegister = errors.New("cannot create QSL readers or writers without calling RegisterApplication first") // ErrAlreadyRegistered indicates an additional attempt to RegisterApplication. var ErrAlreadyRegistered = errors.New("qsl.RegisterApplication should only be called ONCE") // Manager creates reader and writer QMessage generators. type Manager interface { NewReader(string) (Reader, error) NewWriter(string) (Writer, error) } // Message is the QMessage interface for handling and serializing // QMS data. type Message interface { // GetAvroObject returns the map[string]interface{} version of the data. GetAvroObject() map[string]interface{} // GetSerializedBytes attempts to serialize the message as avro binary for publishing. GetSerializedBytes() ([]byte, error) // GetWriterSchema returns the schema used to serialize the message. GetWriterSchema() string // GetMessageID returns the ID of the message and an error if it's unset. GetMessageID() (string, error) // GetTimestamp returns the timestamp of the message creation and an error // if it's unset. GetTimestamp() (int64, error) // GetWriterHost gets the host ($QUALTRICSHOSTNAME) of the message's creator // and an error if it's unset. GetWriterHost() (string, error) // GetWriterApp gets the client name (passed to qsl.RegisterApplication()) // of the message's creator and an error if it's unset. GetWriterApp() (string, error) // GetTraceID() returns the UUIDv4 from the message's creation and an error // if it's unset. GetTraceID() (string, error) // GetOrganizationID() returns the organization ID of the message and an error // if it's neither a string nor a nil. GetOrganizationID() (string, error) // SetWriterSchema changes the writer schema and message ID of the Message or // returns a non-nil error if the Message is Using Original Bytes. SetWriterSchema(string, string) error // SetOrganizationID sets the organization ID of the Message if the Message is // not Using Original Bytes. SetOrganizationID(string) error // SetWriterHost sets the WriterHost ($QUALTRICSHOSTNAME) of the Message if // the Message is not Using Original Bytes. SetWriterHost(string) error // SetTraceID sets the TraceID (defaults to a UUIDv4) of the Message if the // Message is not Using Original Bytes. SetTraceID(string) error // Get returns the value in the payload of the Message at the given key and // an error if the key does not exist in the payload field. Get(string) (interface{}, error) // Set changes the value of the payload of the Message at the given key and // returns an error if the payload key does not exist. Set(string, interface{}) error // GetPayload returns the payload field of the Message and an error if it does // not exist. GetPayload() (map[string]interface{}, error) // GetPayload sets the payload field of the Message and returns an error if // the new payload does not match the Writer Schema. SetPayload(map[string]interface{}) error // GetOriginalBytes returns the serialized avro bytes the message was originally // parsed from and an error if the Message wasn't created by reading avro bytes. GetOriginalBytes() ([]byte, error) // GetReaderSchema returns the schema used to originally parse serialized avro // bytes and an error if the Message wasn't created by reading avro bytes. GetReaderSchema() (string, error) // UseOriginalBytes sets whether the Message should be modifiable and // serializable or if it should use the serialized avro bytes it parsed when // created. Returns an error if set to true when the Message wasn't originally // read from serialized bytes. UseOriginalBytes(bool) error // UsingOriginalBytes indicates whether the Message is modifiable or if it will // only serialize to the bytes originally parsed to create the Message. Always // returns false if the Message wasn't created by reading avro bytes. UsingOriginalBytes() bool } // Reader generates QMessages from encoded avro values. type Reader interface { NewMessage([]byte) (Message, error) } // Writer generates QMessages from native data or existing Messages. type Writer interface { NewMessage() (Message, error) NewFromObject(map[string]interface{}) (Message, error) NewFromMessage(Message) (Message, error) } type schemaLocalManager interface { Get(string) (string, error) } type qmManager struct { appName string schemaManager schemaLocalManager } // RegisterApplication creates a new Manager singleton and starts the version beacon. func RegisterApplication(appName string) error { if Instance != nil { return ErrAlreadyRegistered } var err error var manager Manager manager, err = newManager(appName) if err == nil { once.Do(func() { version := "unknown" name := "qpcl-go-client" beacon.Register(appName, name, version, beacon.MetricsPrefix) Instance = manager }) } return err } // newManager creats a new Manager with a fixed app name. func newManager(appName string) (*qmManager, error) { manager, err := schema.NewManager() if err != nil { return nil, err } return &qmManager{appName: appName, schemaManager: manager}, nil } // GetLibraryVersion reads the file at the given path for a // json formatted "id" (the name) and "version" (the library // version). If any error occurs, such as the path not existing // or being an invalid .json file, then it returns the default // "unknown" version and "serialization" name. func GetLibraryVersion(path string) (string, string) { defaultVersion := "unknown" defaultName := "serialization" jsonBytes, err := ioutil.ReadFile(path) if err != nil { return defaultVersion, defaultName } var versionIDMap map[string]interface{} err = json.Unmarshal(jsonBytes, &versionIDMap) if err != nil { return defaultVersion, defaultName } var name, version string nameValue, ok := versionIDMap["id"] if !ok { name = defaultName } else if nameString, ok := nameValue.(string); !ok { name = defaultName } else { name = nameString } versionValue, ok := versionIDMap["version"] if !ok { version = defaultVersion } else if versionFloat, ok := versionValue.(float64); ok { version = strconv.FormatFloat(versionFloat, 'f', -1, 64) } else { version = defaultVersion } return version, name } // NewReader attempts to create a QMessage Reader that uses the // schema spec specified by the given schema ID. func NewReader(schemaID string) (Reader, error) { if Instance == nil { return nil, ErrNeedRegister } return Instance.NewReader(schemaID) } // qmManager.NewReader attempts to create a Reader with a // given schema. func (q *qmManager) NewReader(schemaID string) (Reader, error) { schemaSpec, err := q.schemaManager.Get(schemaID) if err != nil { return nil, err } return newReader(schemaSpec, q.schemaManager), nil } // NewWriter attempts to create a QMessage Writer that uses the // schema spec specified by the given schema ID. func NewWriter(schemaID string) (Writer, error) { if Instance == nil { return nil, ErrNeedRegister } return Instance.NewWriter(schemaID) } // qmManager.NewWriter attempts to create a Writer with a // given schema. func (q *qmManager) NewWriter(schemaID string) (Writer, error) { schemaSpec, err := q.schemaManager.Get(schemaID) if err != nil { return nil, err } return newWriter(schemaSpec, schemaID, q.appName, q.schemaManager), nil } // Union will create a goavro union given the key and value. func Union(key string, value interface{}) interface{} { return qm.Union(key, value) }