Documentation
¶
Overview ¶
Package jsmu abstracts the typical 2-pass JSON encoding into what looks like a 1-pass JSON encoding step when marshalling or unmarshalling data.
Pronounce as j◦s◦mew.
TypeName, ConcreteType, and 2-Pass Encoding ¶
Writing a general purpose JSON unmarshaller usually requires the following steps:
- json.Unmarshal() into a type containing at least two fields: a. int|string field identifying the desired Go struct destination type; call this the TypeName field. b. json.RawMessage field containing the data or payload that is not yet unmarshalled.
- Type switch or branch off the TypeName field to create a Go struct destination; call this the ConcreteType
- json.Unmarshal() the json.RawMessage into ConcreteType.
Writing a general purpose JSON marshaller works in reverse:
- json.Marshal() the outgoing ConcreteType to json.RawMessage.
- Type switch or branch off the outgoing ConcreteType to create an int|string TypeName identification.
- Pack the outputs from steps 1 and 2 (along with any other desired information) into some type of envelope and json.Marshal() the envelope.
jsmu simply abstracts the above logic into a single marshal or unmarshal call as necessary and alleviates you from having to maintain large type switches, constructor tables, or whatever other mechanism you perform the above logic with.
Register ¶
jsmu requires that ConcreteType(s) be registered along with their TypeName and supports two conventions for supplying the TypeName.
You can embed a TypeName field directly into your struct:
type MyType struct {
jsmu.TypeName `jsmu:"my-type"`
//
String string `json:"string"`
Number int `json:"number"`
}
var mu jsmu.MU
mu.Register(&MyType{})
Or you can pass a TypeName value during the call to Register():
type MyType struct {
String string `json:"string"`
Number int `json:"number"`
}
//
var mu jsmu.MU
mu.Register(&MyType{}, jsmu.TypeName("my-type"))
Messages and Envelopes ¶
Applications want to send messages. In order for an endpoint to unpack an arbitrary message into a concrete type we need to send type information along with the message (this was referred to as TypeName above).
In other words the most basic message format becomes:
{
"type" : int|string // Uniquely describes the type for the "message" field.
"message" : any // The message (broadcast|request|reply) the application wanted to send.
}
Conceptually we can think of this type wrapped around the "message" as the envelope.
jsmu allows you to use any custom type as an envelope by exposing and expecting the Enveloper interface:
type CustomEnvelope struct {
// Must implement jsmu.Enveloper interface
}
my := &jsmu.MU{
EnveloperFn : func() Enveloper{ return &CustomEnvelope{} },
}
If you leave the EnveloperFn field as nil then DefaultEnveloperFunc is used and your JSON needs to conform to the structure defined by Envelope.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrAlreadyRegistered is returned during Register() if the type string is already registered. ErrAlreadyRegistered = fmt.Errorf("already registered") // ErrMissingTypeName is returned during Register() if the type string can not be located. ErrMissingTypeName = fmt.Errorf("missing type name") // ErrNothingRegistered is returned during MU.Marshal() and MU.Unmarshal() when no types have been registered. ErrNothingRegistered = fmt.Errorf("nothing registered") // ErrUnregisteredType is returned during MU.Marshal() and MU.Unmarshal() when the type has not been registered. ErrUnregisteredType = fmt.Errorf("unregistered type") )
var ( // DefaultEnveloperFunc creates and returns an *Envelope from this package. DefaultEnveloperFunc = func() Enveloper { return &Envelope{} } )
var ( // DefaultMarshaller is the Marshaller used when no Marshaller is provided to MU. DefaultMarshaller = &JSONMarshaller{} )
Functions ¶
This section is empty.
Types ¶
type Envelope ¶
type Envelope struct {
Type string `json:"type"`
Id string `json:"id"`
Raw json.RawMessage `json:"message"`
// contains filtered or unexported fields
}
Envelope is a default Enveloper for basic needs.
JSON must conform to this format:
{
"type" : string, // A string uniquely identifying "message".
"id" : string, // An optional unique message ID; provided as a convenience for request-reply message flow.
"message" : {}, // Any JSON data your application can receive.
}
func (*Envelope) GetMessage ¶
func (me *Envelope) GetMessage() interface{}
GetMessage returns the message in the envelope.
func (*Envelope) GetRawMessage ¶
func (me *Envelope) GetRawMessage() json.RawMessage
GetRawMessage returns the raw JSON message in the envelope.
func (*Envelope) GetTypeName ¶
GetTypeName returns the type name string.
func (*Envelope) SetMessage ¶
func (me *Envelope) SetMessage(message interface{})
SetMessage sets the message in the envelope.
func (*Envelope) SetRawMessage ¶
func (me *Envelope) SetRawMessage(raw json.RawMessage)
SetRawMessage sets the raw JSON message in the envelope.
func (*Envelope) SetTypeName ¶
SetTypeName sets the type name string.
type Enveloper ¶
type Enveloper interface {
// GetMessage returns the message in the envelope.
GetMessage() interface{}
// SetMessage sets the message in the envelope.
SetMessage(interface{})
// GetRawMessage returns the raw JSON message in the envelope.
GetRawMessage() json.RawMessage
// SetRawMessage sets the raw JSON message in the envelope.
SetRawMessage(json.RawMessage)
// GetTypeName returns the type name string.
GetTypeName() string
// SetTypeName sets the type name string.
SetTypeName(string)
}
Enveloper is the interface for message Envelopes.
type EnveloperFunc ¶
type EnveloperFunc func() Enveloper
EnveloperFunc is a function that creates a new Enveloper.
type JSONMarshaller ¶
type JSONMarshaller struct{}
JSONMarshaller implements Marshaller with JSON encoding.
func (*JSONMarshaller) Marshal ¶
func (me *JSONMarshaller) Marshal(v interface{}) ([]byte, error)
Marshal marshals v into the expected encoding.
func (*JSONMarshaller) Unmarshal ¶
func (me *JSONMarshaller) Unmarshal(data []byte, v interface{}) error
Unmarshal unmarshals data into v.
type MU ¶
type MU struct {
//
// EnveloperFn is the function that creates a new envelope when one is needed. If not provided
// (aka left as nil) then DefaultEnveloperFunc is used and your JSON must conform to the format
// described for type Envelope.
EnveloperFn EnveloperFunc
//
// Marshaller is the implementation for marshalling and unmarshalling data. If not provided
// (aka left as nil) then a DefaultMarshaller is used; DefaultMarshaller uses json.Marshal() and
// json.Unmarshal().
Marshaller Marshaller
//
// StructTag specifies the struct tag name to use when inspecting types during register. If not
// set will default to "jsmu".
StructTag string
// contains filtered or unexported fields
}
MU marshals|unmarshals JavaScript encoded messages.
Example ¶
package main
import (
"fmt"
"github.com/nofeaturesonlybugs/jsmu"
)
func main() {
// This example demonstrates:
// + Register() when the type information is embedded with jsmu.TypeName.
// + Unmarshal()
type Person struct {
jsmu.TypeName `js:"-" jsmu:"person"`
//
Name string `json:"name"`
Age int `json:"age"`
}
type Animal struct {
jsmu.TypeName `js:"-" jsmu:"animal"`
//
Name string `json:"name"`
Says string `json:"says"`
}
//
mu := &jsmu.MU{
// EnveloperFn: nil, // jsmu.DefaultEnveloperFunc is the default.
// StructTag : "", // "jsmu" is the default.
}
mu.MustRegister(&Person{})
mu.MustRegister(&Animal{})
//
strings := []string{
`{
"type" : "person",
"message" : {
"name" : "Bob",
"age" : 40
}
}`,
`{
"type" : "animal",
"message" : {
"name" : "cat",
"says" : "meow"
}
}`,
`{
"type" : "person",
"message" : {
"name" : "Sally",
"age" : 30
}
}`,
`{
"type" : "animal",
"message" : {
"name" : "cow",
"says" : "moo"
}
}`,
}
var envelope jsmu.Enveloper
var err error
for _, str := range strings {
if envelope, err = mu.Unmarshal([]byte(str)); err != nil {
fmt.Println(err)
return
}
switch message := envelope.GetMessage().(type) {
case *Animal:
fmt.Printf("A %v says, \"%v.\"\n", message.Name, message.Says)
case *Person:
fmt.Printf("%v is %v year(s) old.\n", message.Name, message.Age)
}
}
}
Output: Bob is 40 year(s) old. A cat says, "meow." Sally is 30 year(s) old. A cow says, "moo."
Example (ApiRequest) ¶
package main
import (
"fmt"
"github.com/nofeaturesonlybugs/jsmu"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
type ApiGetPerson struct {
jsmu.TypeName `json:"-" jsmu:"get/person"`
//
Id int `json:"id"`
Person *Person `json:"person"`
}
func main() {
//
people := map[int]*Person{
10: {
Name: "Bob",
Age: 40,
},
20: {
Name: "Sally",
Age: 32,
},
}
//
mu := &jsmu.MU{}
mu.MustRegister(&ApiGetPerson{})
//
strings := []string{
`{
"type" : "get/person",
"id" : "first",
"message" : {
"id" : 20
}
}`,
`{
"type" : "get/person",
"id" : "second",
"message" : {
"id" : 10
}
}`,
}
var envelope jsmu.Enveloper
var response []byte
var err error
for _, str := range strings {
if envelope, err = mu.Unmarshal([]byte(str)); err != nil {
fmt.Println(err)
return
}
switch message := envelope.GetMessage().(type) {
case *ApiGetPerson:
if person, ok := people[message.Id]; ok {
message.Person = person
}
}
if response, err = mu.Marshal(envelope); err != nil {
fmt.Println(err)
return
}
fmt.Printf("%v\n", string(response))
}
}
Output: {"type":"get/person","id":"first","message":{"id":20,"person":{"name":"Sally","age":32}}} {"type":"get/person","id":"second","message":{"id":10,"person":{"name":"Bob","age":40}}}
Example (Arrays) ¶
package main
import (
"fmt"
"github.com/nofeaturesonlybugs/jsmu"
)
func main() {
// This example shows how to register slices.
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
type Animal struct {
Name string `json:"name"`
Says string `json:"says"`
}
//
mu := &jsmu.MU{}
mu.MustRegister([]*Person{}, jsmu.TypeName("people"))
mu.MustRegister([]*Animal{}, jsmu.TypeName("animals"))
//
strings := []string{
`{
"type" : "people",
"message" : [{
"name" : "Bob",
"age" : 40
}, {
"name" : "Sally",
"age" : 30
}]
}`,
`{
"type" : "animals",
"message" : [{
"name" : "cat",
"says" : "meow"
}, {
"name" : "cow",
"says" : "moo"
}]
}`,
}
var envelope jsmu.Enveloper
var err error
for _, str := range strings {
if envelope, err = mu.Unmarshal([]byte(str)); err != nil {
fmt.Println(err)
return
}
switch message := envelope.GetMessage().(type) {
case []*Animal:
for _, animal := range message {
fmt.Printf("A %v says, \"%v.\"\n", animal.Name, animal.Says)
}
case []*Person:
for _, person := range message {
fmt.Printf("%v is %v year(s) old.\n", person.Name, person.Age)
}
}
}
var buf []byte
people := []*Person{
{Name: "Larry", Age: 42},
{Name: "Curly", Age: 48},
{Name: "Moe", Age: 46},
}
if buf, err = mu.Marshal(people); err != nil {
fmt.Println(err)
}
fmt.Printf("%v\n", string(buf))
}
Output: Bob is 40 year(s) old. Sally is 30 year(s) old. A cat says, "meow." A cow says, "moo." {"type":"people","id":"","message":[{"name":"Larry","age":42},{"name":"Curly","age":48},{"name":"Moe","age":46}]}
Example (CustomEnvelope) ¶
package main
// This example demonstrates using a custom envelope with a JSON structure not expected by jsmu.Envelope.
import (
"encoding/json"
"fmt"
"github.com/nofeaturesonlybugs/jsmu"
)
// CustomEnvelope is for JSON that has a custom structure.
type CustomEnvelope struct {
TypeInfo string `json:"typeInfo"`
Payload json.RawMessage `json:"payload"`
CustomA string `json:"customA"`
CustomB string `json:"customB"`
data interface{} `json:"-"`
}
// GetMessage returns the message in the envelope.
func (me *CustomEnvelope) GetMessage() interface{} {
return me.data
}
// SetMessage sets the message in the envelope.
func (me *CustomEnvelope) SetMessage(message interface{}) {
me.data = message
}
// GetRawMessage returns the raw JSON message in the envelope.
func (me *CustomEnvelope) GetRawMessage() json.RawMessage {
return me.Payload
}
// SetRawMessage sets the raw JSON message in the envelope.
func (me *CustomEnvelope) SetRawMessage(raw json.RawMessage) {
me.Payload = raw
}
// GetTypeName returns the type name string.
func (me *CustomEnvelope) GetTypeName() string {
return me.TypeInfo
}
// SetTypeName sets the type name string.
func (me *CustomEnvelope) SetTypeName(typeInfo string) {
me.TypeInfo = typeInfo
}
func main() {
type Person struct {
jsmu.TypeName `js:"-" jsmu:"person"`
//
Name string `json:"name"`
Age int `json:"age"`
}
type Animal struct {
jsmu.TypeName `js:"-" jsmu:"animal"`
//
Name string `json:"name"`
Says string `json:"says"`
}
//
mu := &jsmu.MU{
EnveloperFn: func() jsmu.Enveloper {
return &CustomEnvelope{}
},
// StructTag : "", // "jsmu" is the default.
}
mu.MustRegister(&Person{})
mu.MustRegister(&Animal{})
//
strings := []string{
`{
"typeInfo" : "person",
"payload" : {
"name" : "Bob",
"age" : 40
},
"customA" : "Hello!",
"customB" : "Goodbye!"
}`,
`{
"typeInfo" : "animal",
"payload" : {
"name" : "cat",
"says" : "meow"
},
"customA" : "Foo",
"customB" : "Bar"
}`,
`{
"typeInfo" : "person",
"payload" : {
"name" : "Sally",
"age" : 30
},
"customA" : "qwerty",
"customB" : "dvorak"
}`,
`{
"typeInfo" : "animal",
"payload" : {
"name" : "cow",
"says" : "moo"
},
"customA" : "Batman",
"customB" : "Robin"
}`,
}
var envelope jsmu.Enveloper
var err error
for _, str := range strings {
if envelope, err = mu.Unmarshal([]byte(str)); err != nil {
fmt.Println(err)
return
}
custom := envelope.(*CustomEnvelope)
switch message := envelope.GetMessage().(type) {
case *Animal:
fmt.Printf("A %v says, \"%v;\" [%v,%v].\n", message.Name, message.Says, custom.CustomA, custom.CustomB)
case *Person:
fmt.Printf("%v is %v year(s) old; [%v,%v].\n", message.Name, message.Age, custom.CustomA, custom.CustomB)
}
}
}
Output: Bob is 40 year(s) old; [Hello!,Goodbye!]. A cat says, "meow;" [Foo,Bar]. Sally is 30 year(s) old; [qwerty,dvorak]. A cow says, "moo;" [Batman,Robin].
Example (MessageFactory) ¶
package main
import (
"fmt"
"github.com/nofeaturesonlybugs/jsmu"
)
func main() {
// This example shows how to provide a constructor or factory function for instantiating
// your types if necessary.
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
// We create a constructor function compatible with jsmu.MessageFunc.
NewPerson := func() (interface{}, error) {
fmt.Println("Created a person!")
return &Person{}, nil
}
//
mu := &jsmu.MU{
// EnveloperFn: nil, // jsmu.DefaultEnveloperFunc is the default.
// StructTag : "", // "jsmu" is the default.
}
// Pass the NewPerson constructor during registration.
mu.MustRegister(&Person{}, jsmu.MessageFunc(NewPerson), jsmu.TypeName("person"))
//
strings := []string{
`{
"type" : "person",
"message" : {
"name" : "Bob",
"age" : 40
}
}`,
`{
"type" : "person",
"message" : {
"name" : "Sally",
"age" : 30
}
}`,
}
var envelope jsmu.Enveloper
var err error
for _, str := range strings {
if envelope, err = mu.Unmarshal([]byte(str)); err != nil {
fmt.Println(err)
return
}
switch message := envelope.GetMessage().(type) {
case *Person:
fmt.Printf("%v is %v year(s) old.\n", message.Name, message.Age)
}
}
}
Output: Created a person! Bob is 40 year(s) old. Created a person! Sally is 30 year(s) old.
Example (WithoutEmbed) ¶
package main
import (
"fmt"
"github.com/nofeaturesonlybugs/jsmu"
)
func main() {
// This example does not embed jsmu.TypeName into the registered structs and
// instead passes that information in the call to Register().
//
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
type Animal struct {
Name string `json:"name"`
Says string `json:"says"`
}
//
mu := &jsmu.MU{}
mu.MustRegister(&Person{}, jsmu.TypeName("person"))
mu.MustRegister(&Animal{}, jsmu.TypeName("animal"))
//
strings := []string{
`{
"type" : "person",
"message" : {
"name" : "Bob",
"age" : 40
}
}`,
`{
"type" : "animal",
"message" : {
"name" : "cat",
"says" : "meow"
}
}`,
`{
"type" : "person",
"message" : {
"name" : "Sally",
"age" : 30
}
}`,
`{
"type" : "animal",
"message" : {
"name" : "cow",
"says" : "moo"
}
}`,
}
var envelope jsmu.Enveloper
var err error
for _, str := range strings {
if envelope, err = mu.Unmarshal([]byte(str)); err != nil {
fmt.Println(err)
return
}
switch message := envelope.GetMessage().(type) {
case *Animal:
fmt.Printf("A %v says, \"%v.\"\n", message.Name, message.Says)
case *Person:
fmt.Printf("%v is %v year(s) old.\n", message.Name, message.Age)
}
}
}
Output: Bob is 40 year(s) old. A cat says, "meow." Sally is 30 year(s) old. A cow says, "moo."
func (*MU) Marshal ¶
Marshal marshals the incoming value. If value is already an Enveloper then MU.Marshaller.Marshal(value) is returned. Otherwise value is wrapped in a new envelope and MU.Marshaller.Marshal(NewEnvelope{value}) is returned.
func (*MU) MustRegister ¶
func (me *MU) MustRegister(value interface{}, opts ...interface{})
MustRegister is similar to Register() except it panic if an error is returned.
type Marshaller ¶
type Marshaller interface {
// Marshal marshals v into the expected encoding.
Marshal(v interface{}) ([]byte, error)
// Unmarshal unmarshals data into v.
Unmarshal(data []byte, v interface{}) error
}
Marshaller is the interface for marshalling data.
type MessageFunc ¶
type MessageFunc func() (interface{}, error)
MessageFunc is a function that instantiates a type.
type MockMarshaller ¶
type MockMarshaller struct {
MarshalImplementation func(v interface{}) ([]byte, error)
UnmarshalImplementation func(data []byte, v interface{}) error
}
MockMarshaller implements Marshaller but allows you to swap out the implementation with functions.
Consider using this during unit testing.
func (*MockMarshaller) Marshal ¶
func (me *MockMarshaller) Marshal(v interface{}) ([]byte, error)
Marshal marshals v into the expected encoding.
func (*MockMarshaller) Unmarshal ¶
func (me *MockMarshaller) Unmarshal(data []byte, v interface{}) error
Unmarshal unmarshals data into v.