Last active
May 19, 2021 17:27
-
-
Save shannonwells/8a933210213c1ea260a90f456513e9c3 to your computer and use it in GitHub Desktop.
Reflections in Go, For Cats
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package reflect_cats_test | |
import ( | |
"encoding/json" | |
"net/url" | |
"reflect" | |
"testing" | |
"github.com/stretchr/testify/assert" | |
"github.com/stretchr/testify/require" | |
) | |
// Blog post here: https://blog.carbonfive.com/2020/04/07/reflections-in-go-for-cats/ | |
// As stated in my blog post, to make this more palatable, | |
// I've contrived a slightly different set of requirements from | |
// what I had to implement. | |
// First, spend a few minutes reading over the implementations and | |
// interface at the bottom of this gist. | |
// We will receive bytes that we expect to convert to a Pet | |
//(see implementations at the bottom of this file), but we don't know in | |
// advance what Type of Pet it is. We need to make an empty <someType> | |
//struct and call Pet.Adopt() on it. | |
// | |
// The deserialization function signature is: | |
// `func(t reflect.Type, data []byte) Pet` | |
// | |
func TestHowToUseReflections(t *testing.T) { | |
// First we'll look at reflections generally. Say we have an initialized Cat: | |
cat := Cat{} | |
// Let's look at what TypeOf gives us. This is the parameter | |
// type we must use in our deserialization function. | |
catPtrType := reflect.TypeOf(&cat) // returns a reflect.Type | |
assert.Equal(t, "*reflect_cats_test.Cat", catPtrType.String()) | |
// Why aren't we looking at reflect.TypeOf(cat)? | |
// Look at the methods of Cat which fulfill the Pet interface: | |
// func (f *Cat) IsVaccinated() bool | |
// These functions take pointer receivers. We must call the Pet | |
// interface functions on a pointer to a Cat, and the Registrar | |
// expects pointer types. | |
// You could, if you want, not use pointer receivers, but this | |
// discussion could be its own blog post. Experiment with it | |
// in a separate test of your own. | |
// Continuing, if we want to instantiate a struct via its type, we should | |
// use reflect.New somehow. godoc says, "New returns a Value | |
// representing a pointer to a new zero value for the specified type." | |
// That is, the returned Value's Type is PtrTo(typ) | |
aCatPtrTypeVal := reflect.New(catPtrType) | |
// We now have a Value that is a pointer to a pointer. Illustrative, | |
// but not very useful; it's going the opposite direction of | |
// what we want, which is an empty struct. | |
assert.Equal(t, "<**reflect_cats_test.Cat Value>", aCatPtrTypeVal.String()) | |
// Let's get back to the thing we want to initialize -- a Cat. | |
// For pointers, Elem() and Indirect() return the same thing: | |
aCatPtrVal := reflect.Indirect(aCatPtrTypeVal) // this should be a *Cat | |
assert.Equal(t, aCatPtrVal, aCatPtrTypeVal.Elem()) // yes, it is a *Cat | |
// We now have a reflect.Value containing a pointer to a Cat: | |
assert.Equal(t, "<*reflect_cats_test.Cat Value>", aCatPtrVal.String()) | |
// reflect.New(thingType) creates a reflect.Value, containing a pointer to | |
// the zero-value version of a thing of Type thingType. | |
// | |
// If thingType is a Kind of pointer, it creates a real pointer to nil, i.e., | |
// &(nil) | |
// NOT a real pointer to a Zero value of what typeThing points to, i.e., | |
// NOT &(&thing{}) | |
assert.False(t, aCatPtrTypeVal.IsNil()) // it's a non-nil address | |
assert.True(t, aCatPtrTypeVal.Elem().IsNil()) // that points to a nil address for a Cat | |
// If what you want is &(&thing{}), you must call Set, using reflect.New on | |
// catPtrType.Elem(), where catPtrType.Elem points to a type | |
// that the pointer points to. | |
// Here we set aCatPtrVal to the Value of a pointer to an | |
// empty/zero-value Cat. | |
// To get the empty Cat struct from this, we call Value.Elem, | |
// which gives us the child Value that the pointer contains. | |
// So, catPtrType is a TypeOf *Cat, and catPtrType.Elem() gives us a TypeOf Cat | |
catType := catPtrType.Elem() | |
// New initializes an empty struct. Note reflect.New returns a pointer | |
// to a -- Value -- of the provided type. | |
aCatPtrVal2 := reflect.New(catType) | |
// You can do the same by calling Set on an existing pointer: | |
aCatPtrVal.Set(reflect.New(catType)) | |
assert.NotEqual(t, aCatPtrVal, aCatPtrVal2) // The two addresses are different, | |
// but the struct values are the same. We check the Cat struct fields by | |
// calling aCatPtrVal.Elem() : | |
assert.Equal(t, "", aCatPtrVal.Elem().FieldByName("Name").String()) | |
assert.Equal(t, "", aCatPtrVal2.Elem().FieldByName("Name").String()) | |
assert.Equal(t, "<bool Value>", aCatPtrVal.Elem().FieldByName("Vaccinated").String()) | |
assert.Equal(t, "<bool Value>", aCatPtrVal2.Elem().FieldByName("Vaccinated").String()) | |
// Note we can't ask about struct fields that don't exist: | |
assert.Equal(t, | |
"<invalid Value>", | |
aCatPtrVal.Elem().FieldByName("Nonexistentfield").String()) | |
// Then we call Value.Interface to give us thing itself | |
// as a reflect.Value. Here is what we wanted in the first place: | |
// an empty Cat struct. Well this certainly a roundabout way to get there. | |
assert.Equal(t, cat, aCatPtrVal.Elem().Interface()) | |
// This checks the same thing as above: | |
assert.True(t, aCatPtrVal2.Elem().IsZero()) | |
// Let's see if we can call Pet interface funcs. | |
aPet, ok := aCatPtrVal2.Interface().(Pet) | |
require.True(t, ok) // verify the cast worked | |
require.NotNil(t, aPet) // otherwise the linter warns we didn't do a nil check | |
// Now we are getting somewhere. | |
// make a JSON-serialized Cat and check that we can deserialize it. | |
shelterPet := `{"name":"Lily","vaccinated":true}` | |
require.NoError(t, aPet.Adopt([]byte(shelterPet))) | |
// Try calling more interface functions | |
assert.True(t, aPet.IsVaccinated()) | |
assert.Equal(t, "Lily", aPet.PetName()) // woo! our deserialize worked! | |
// Can we play some more? Let's cast to Cat so we can directly | |
// examine Cat things. | |
aCat, ok := aCatPtrVal.Interface().(Cat) | |
typeOfACatPtrValInterface := reflect.TypeOf(aCat) | |
// ^^ What is this thing's type? | |
// It's a Cat. | |
assert.Equal(t, "reflect_cats_test.Cat", typeOfACatPtrValInterface.String()) | |
// We can now call Adopt and prove that it worked: | |
require.NoError(t, aCat.Adopt([]byte(shelterPet))) | |
// We can also now look at Cat.Cat-specific fields and funcs: | |
assert.Equal(t, "Lily", aCat.Name) | |
assert.Equal(t, "meow!", aCat.Sound()) | |
} | |
// Now that we've done some exploratory playing around, | |
// implement what we have learned. | |
func TestImplementation(t *testing.T) { | |
catPtrType := reflect.TypeOf(&Cat{}) | |
petStream := `{"name":"Freddie","type":"Felis Catus"}` | |
// our function takes a type, and some data, and combines | |
// these two to return a Pet, if possible. | |
// if it's not possible, it returns nil. | |
AdoptFunc := func(incomingType reflect.Type, data []byte) Pet { | |
// incomingType is expected to be a pointer Kind. You can do a safety check: | |
if incomingType.Kind() != reflect.Ptr { | |
return nil | |
} | |
// From godoc: "Elem returns the value that the interface v | |
// contains or that the pointer v points to." | |
// So here reflect.New(incomingType.Elem()) is the same as saying | |
// | |
// vStructPtr := reflect.ValueOf(&Cat{}) | |
// | |
// Since this function doesn't know at compile time what | |
// to deserialize, we use incomingType.Elem() | |
vStructPtr := reflect.New(incomingType.Elem()) | |
// vStructPtr.Interface() gives us a *Cat. Check that | |
// it casts to a Pet interface. | |
pet, ok := vStructPtr.Interface().(Pet) | |
if !ok { | |
return nil | |
} | |
if err := pet.Adopt(data); err != nil { | |
return nil | |
} | |
return pet | |
} | |
res := AdoptFunc(catPtrType, []byte(petStream)) | |
require.NotNil(t, res) | |
// Verify the Pet functions' outputs | |
assert.Equal(t, "Freddie", res.PetName()) | |
assert.Equal(t, "Felis Catus", res.Species()) | |
assert.False(t, res.IsVaccinated()) | |
// verify that we can send AdoptFunc bad input without | |
// panicking. | |
// 1. try with the wrong type, e.g. a non-pointer type. | |
urlType := reflect.TypeOf(url.URL{}) | |
res = AdoptFunc(urlType, []byte(petStream)) | |
assert.Nil(t, res) | |
// 2. try with json data that will not serialize to a Cat. | |
res = AdoptFunc(catPtrType, []byte(`"garbage":"moregarbage"`)) | |
assert.Nil(t, res) | |
// 3. try with a pointer type that doesn't implement Pet, | |
// but otherwise looks sort of like a Cat type struct. | |
type Borked struct { | |
Name string `json:"name"` | |
} | |
borkedPtrType := reflect.TypeOf(&Borked{}) | |
res = AdoptFunc(borkedPtrType, []byte(petStream)) | |
assert.Nil(t, res) | |
} | |
// There was another requirement for this story, which was to allow | |
// registration of types with validation functions. | |
// | |
// We have Pets which must be registered by their owners, including a | |
// Veterinarian that must be able to Examine the Pet and vaccinate them. | |
// If Examine is successful, then IsVaccinated should return true. | |
// Simulate getting a pet from a shelter by deserializing bites, | |
// I mean, bytes, into a Pet. | |
// The rest of this exercise is left to the reader -- make the tests pass! | |
func TestRegistration(t *testing.T) { | |
cv := &CatVet{} | |
r := Registrar{} | |
t.Run("Can register but not re-register *Cat with Registrar", func(t *testing.T) { | |
tpf := reflect.TypeOf(&Cat{}) | |
assert.NoError(t, r.RegisterPet(tpf, cv)) | |
assert.Len(t, r.RegisteredPets, 1) | |
err := r.RegisterPet(tpf, cv) | |
assert.EqualError(t, err, "already registered Felis Catus") | |
}) | |
t.Run("Attempting to register a non-Pet returns an error", func(t *testing.T) { | |
type notAPet struct{ | |
Name string | |
} | |
err := r.RegisterPet(reflect.TypeOf(notAPet{ Name: "Chimpanzee" }), cv) | |
assert.EqualError(t, err, "not a Pet") | |
}) | |
t.Run("returns nil, error if Pet is not a pointer", func(t *testing.T) { | |
tf := reflect.TypeOf(Cat{}) | |
err := r.RegisterPet(tf, cv) | |
assert.EqualError(t, err, "reflect_cats_test.Cat is not a pointer") | |
}) | |
t.Run("Dog is a Pet and can be 'adopted' too", func(t *testing.T){ | |
// If you like, make the tests pass for a Dog struct of your own | |
// design. | |
type Dog struct {} | |
// var _ Pet = &Dog | |
}) | |
} | |
func TestAdopt(t *testing.T) { | |
cv := &CatVet{} | |
r := &Registrar{} | |
cat := &Cat{} | |
t.Run("Registered pet is examined", func(t *testing.T) { | |
require.NoError(t, r.RegisterPet(reflect.TypeOf(cat), cv)) | |
petStream := `{"name":"Bili","type":"Felis Catus"}` | |
t.Run("returns (Pet, nil) if examine succeeds.", func(t *testing.T){ | |
pet, err := r.Adopt([]byte(petStream)) | |
require.NoError(t, err) | |
assert.NotNil(t, pet) | |
assert.True(t, cat.IsVaccinated()) | |
}) | |
t.Run("returns error if examine fails", func(t *testing.T){ | |
t.Fail() | |
}) | |
}) | |
t.Run("can register multiple species", func(t *testing.T){ | |
t.Fail() | |
}) | |
t.Run("returns nil, error if the type is not registered", func(t *testing.T) { | |
t.Fail() | |
}) | |
t.Run("returns nil, error if the data cannot be unmarshaled", func(t *testing.T) { | |
t.Fail() | |
}) | |
t.Run("returns nil, error if the data cannot be unmarshaled", func(t *testing.T) { | |
t.Fail() | |
}) | |
} | |
// ======== IMPLEMENTATION ======= | |
// --- INTERFACES --- | |
// Pet is a domesticated animal companion for humans. It can be | |
// "adopted" via Adopt. | |
type Pet interface { | |
IsVaccinated() bool | |
IsHealthy() bool | |
Adopt([]byte) error | |
PetName() string | |
Species() string | |
Vaccinate() | |
} | |
// --- TYPES --- | |
// Cat implements Pet | |
type Cat struct { | |
// Must be exported field or JSON will not marshal/unmarshal. | |
Name string `json:"name"` | |
Healthy bool `json:"healthy"` | |
Vaccinated bool `json:"vaccinated"` | |
} | |
// IsVaccinated returns the boolean value of f.Vaccinated | |
func (f *Cat) IsVaccinated() bool { return f.Vaccinated } | |
func (f *Cat) IsHealthy() bool { return f.Healthy } | |
// Adopt deserializes data into f. | |
func (f *Cat) Adopt(data []byte) error { return json.Unmarshal(data, f) } | |
// PetName returns the Pet's name. | |
func (f *Cat) PetName() string { return f.Name } | |
// Sound | |
func (f Cat) Sound() string { return "meow!" } | |
func (f Cat) Vaccinate() { f.Vaccinated = true } | |
// Species returns the string to use to register a type in the Registrar's | |
// map of allowed types. | |
func (f *Cat) Species() string { return "Felis Catus" } | |
// This construct is used to ensure Cat implements the Pet interface. | |
// If it doesn't, this doesn't compile. | |
var _ Pet = &Cat{} | |
// Veterinarian interface | |
type Veterinarian interface { | |
Examine(pet Pet) bool | |
} | |
type CatVet struct {} | |
func (cv *CatVet)Examine(pet Pet) bool { | |
if pet.IsHealthy() { | |
pet.Vaccinate() | |
return true | |
} | |
return false | |
} | |
// Simple registrar | |
type Registrar struct { | |
// RegisteredPets are mapped by their species. You could add a field in | |
// the Veterinarian struct that indicates what species the Veterinarian | |
// can Examine. | |
RegisteredPets map[string]Veterinarian | |
} | |
// RegisterPet registers Pets to Examine, with their Veterinarian. | |
// Registrar expects all Pet functions to have pointer receivers. | |
// (Not Labrador receivers) Returns error if: | |
// * the type is already registered | |
// * the type does not implement Pet | |
// * the type is not a pointer | |
func (r *Registrar) RegisterPet(t reflect.Type, vet Veterinarian) error { | |
panic("implement me") | |
return nil | |
} | |
// Adopt takes bytes and attempts to convert them into a Pet | |
// Returns nil + error if: | |
// * the Pet type is not registered | |
// * the pet data cannot be read | |
// * Examine returns false | |
func (r *Registrar) Adopt(petData []byte) (Pet, error) { | |
panic("implement me") | |
return nil, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment