reflections_cats_test.go

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 }
Reflections in Go, For Cats

Be the first to comment

You can use [html][/html], [css][/css], [php][/php] and more to embed the code. Urls are automatically hyperlinked. Line breaks and paragraphs are automatically generated.