Browse Source

Released Glom-Go

  Glom-Go is a MIT product. Make sure you check err, most of the time it
will tell you where/what failed.
Apollo 3 years ago
commit
a2ca6f4933
10 changed files with 717 additions and 0 deletions
  1. 176 0
      README.md
  2. 61 0
      example/example.go
  3. 11 0
      example/go.mod
  4. 2 0
      example/go.sum
  5. 4 0
      glom/cover
  6. 53 0
      glom/cover.out
  7. 155 0
      glom/glom.go
  8. 247 0
      glom/glom_test.go
  9. 6 0
      glom/go.mod
  10. 2 0
      glom/go.sum

+ 176 - 0
README.md

@@ -0,0 +1,176 @@
+# Glom-Go
+
+This is based on the Python module/package [glom](https://pypi.org/project/glom/).
+
+## Installation
+
+Should be `go get red-green/glom` (Should also require getting [structs](https://pkg.go.dev/github.com/fatih/structs))
+
+## Basic Example
+
+```go
+/* For brevity I will be showing a basic structure in a Pythonic format (Looks simular to JSON or HJSON)
+
+// See Example Data section below for how this structure would look in Go.
+
+data = { // Assume data is map[string]interface{}
+    "users": {
+    	"Bob": {
+        	"last-on": "some time ago",
+        	"age": 42,
+        	"sec-level": 99
+    	},
+    	"Phil": {
+	          "last-on": "a few seconds ago",
+    	      "age": 0,
+        	  "sec-level": 10
+      	}
+    },
+    "posts": [
+        {
+            "title": "Why Glom-Go Rocks!",
+            "post-date": "a few weeks ago",
+            "description": "Access nested structures with ease, especially mixed types like map, slice/array, and interface.",
+            "line-count": 1,
+            "likes": 0
+        },
+        {
+            "title": "Example of Glom-Go, and 5 other neat tips",
+            "post-date": "a few months ago",
+            "description": "Example in example... See recursion.",
+            "line-count": 1,
+            "likes": 0
+        }
+    ]
+}
+*/
+
+// Let's start with something simple, getting the last time Bob was on...
+bob_last_on, err := glom.Glom(data, "users.Bob.laston")
+// In Python it would be data["users"]["Bob"]["laston"] (But in Go, we can't do that, due to our base type of data... interface)
+
+if err != nil {
+    fmt.Println(err) // Oops, and error occured, Looks like the error would be something like...
+    // Failed moving to 'laston' from path of 'users.Bob', options are 'last-on', 'age', 'sec-level' (3)
+    // So as the error is trying to let us know, we miss typed last-on with laston.
+} else {
+    fmt.Printf("Bob was last on %v.", bob_last_on) // If you fixed it so the string passed to glom.Glom was "users.Bob.last-on"
+    // You now get:
+    // Bob was last on some time ago.
+}
+
+// Now let's access the likes of the second post...
+second_post_likes, err := glom.Glom(data, "posts.1.likes") // Remember slices/arrays start at 0, so the index of 1 will give us the second.
+
+if err != nil {
+    fmt.Println(err)
+} else {
+    fmt.Printf("Post 2 got %v likes.", second_post_likes)
+    // Post 2 got 0 likes.
+}
+```
+
+
+
+## Example Data
+
+> This part was separated due to it's length and complexity. (It is added to allow the above example to actually work, just copy this then copy the Basic Example, above)
+
+```go
+// Initalize a multi-layer structure (I will try my best to show the layers deep, where 1 is directly accessable)
+data := make(map[string]interface{})
+
+// 1 can be like `data["users"]`
+// 2 (which can't be directly accessed without some interface casting) can be `data["users"]["Bob"]`
+// 3 (which can't be directly accessed without some nested interface casting) can be `data["users"]["Bob"]["sec-level"]`
+
+users := make(map[string]interface{}) // 1
+
+bob := make(map[string]interface{}) // 2
+bob["last-on"] = "some time ago" // 3
+bob["age"] = 42 // 3
+bob["sec-level"] = 99 // 3
+users["Bob"] = bob // Add Bob to users
+
+phil := make(map[string]interface{}) // 2
+phil["last-on"] = "a few seconds ago" // 3
+phil["age"] = 0 // 3
+phil["sec-level"] = 10 // 3
+users["Phil"] = phil // Add Phil to users
+
+data["users"] = users // Add users to data
+
+var posts []interface{} // 1
+
+post1 := make(map[string]interface{}) // 2
+post1["title"] = "Why Glom-Go Rocks!" // 3
+post1["post-date"] = "a few weeks ago" // 3
+post1["description"] = "Access nested structures with ease, especially mixed types like map, slice/array, and interface." // 3
+post1["line-count"] = 1 // 3
+post1["likes"] = 0 // 3
+posts = append(posts, post1) // Add post1 to posts
+
+post2 := make(map[string]interface{}) // 2
+post2["title"] = "Example of Glom-Go, and 5 other neat tips" // 3
+post2["post-date"] = "a few months ago" // 3
+post2["description"] = "Example in example... See recursion." // 3
+post2["line-count"] = 1 // 3
+post2["likes"] = 0 // 3
+posts = append(posts, post2) // Add post2 to posts
+
+data["posts"] = posts // Add posts to data
+
+// data is now ready for glom.Glom
+
+```
+
+## Basic Accessing
+
+Python's glom made accessing nested structures a breeze, Glom-Go was built to attempt to do just that just for Go.
+
+* Just use dot notation for accessing/walking your data. (I.E. `users.Bob.age` would access the 3rd level, and comes with Python's glom error messaging showing exactly where while it walked the data it got lost at/couldn't go)
+* Special star for dot notation. (I.E. to get all the users you could `users` or even `users.*`)
+* Glom-Go can support maps, slices/arrays, and structures (specifically it's fields that are public/exposed), making it rather extensive.
+
+## Under the hood
+
+Glom-Go uses reflect and [structs](https://pkg.go.dev/github.com/fatih/structs) to handle nesting thru various structures.
+
+From `[]interface{}` to `map[string]interface{}` (Even `map[int]interface{}` works).
+
+All while supporting custom structures...
+
+```go
+type User struct {
+    Name string
+    Last_on string
+    Age int
+}
+
+type Post struct {
+    Title string
+    Description string
+    Line_count int
+    Author User
+    Post_date string
+}
+
+type Blog struct {
+    Site_name string
+    Posts []Post
+    Site_owner User
+}
+bob := User{"Bob", "some time ago", 42}
+blog := Blog{Site_name: "Test Site", Site_Owner: bob}
+blog.Posts = append(blog.Posts, Post{"Example of Structs with Glom-Go", "Yet another example of Glom-Go", 1, bob, "a few seconds ago"})
+
+// Example of accessing that to get first post's author
+post_owner, err := glom.Glom(blog, "Posts.0.Author.Name")
+if err != nil {
+    fmt.Println(err)
+} else {
+    fmt.Printf("%v wrote the first post!", post_owner)
+    // Bob wrote the first post!
+}
+```
+

+ 61 - 0
example/example.go

@@ -0,0 +1,61 @@
+package main
+
+import (
+	"fmt"
+	"red-green/glom"
+)
+
+func main() {
+
+	// An example structure (Shown using all maps, but slice/array and even structures are supported)
+	critters := make(map[string]interface{})
+	cat := make(map[string]interface{})
+	cat["name"] = "Cat"
+	cat["sounds"] = "Meow"
+	cat["food"] = "Fish"
+	critters["Cat"] = cat
+	dog := make(map[string]interface{})
+	dog["name"] = "Dog"
+	dog["sounds"] = "Woof"
+	dog["food"] = "Anything"
+	critters["Dog"] = dog
+
+	test := make(map[string]interface{})
+	test["Animals"] = critters
+
+	/* In JSON it would be represented as:
+	{
+		"Animals": {
+			"Cat": {
+				"name": "Cat",
+				"sounds": "Meow",
+				"food": "Fish"
+			},
+			"Dog": {
+				"name": "Dog",
+				"sounds": "Woof",
+				"food": "Anything"
+			}
+		}
+	}
+	Where accessing name would be
+	data["Animals"]["Cat"]["name"] or data["Animals"]["Dog"]["name"]
+
+	But with glom-go it's
+	"Animals.Cat.name" or "Animals.Dog.name"
+	*/
+
+	// An example of accessing something that doesn't exist
+	_, err := glom.Glom(test, "Animals.Dog.hates")
+	if err != nil {
+		fmt.Println(err) // Failed moving to 'hates' from path of 'Animals.Dog', options are 'name', 'sounds', 'food' (3)
+	}
+
+	// An example of successfully geting something
+	value, err := glom.Glom(test, "Animals.Cat.sounds")
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Printf("Cat's make '%v' sounds.\r\n", value)
+	}
+}

+ 11 - 0
example/go.mod

@@ -0,0 +1,11 @@
+module red-green/glom_example
+
+go 1.17
+
+replace red-green/glom => ../glom
+
+require (
+	github.com/fatih/structs v1.1.0 // indirect
+	red-green/glom v0.0.0-00010101000000-000000000000 // indirect
+)
+

+ 2 - 0
example/go.sum

@@ -0,0 +1,2 @@
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=

+ 4 - 0
glom/cover

@@ -0,0 +1,4 @@
+#!/bin/bash
+
+go test -coverprofile=cover.out
+go tool cover -html=cover.out

+ 53 - 0
glom/cover.out

@@ -0,0 +1,53 @@
+mode: set
+red-green/glom/glom.go:13.71,15.32 2 1
+red-green/glom/glom.go:18.2,18.37 1 1
+red-green/glom/glom.go:22.2,24.22 3 1
+red-green/glom/glom.go:29.2,29.20 1 1
+red-green/glom/glom.go:15.32,17.3 1 1
+red-green/glom/glom.go:18.37,20.3 1 1
+red-green/glom/glom.go:24.22,27.3 1 1
+red-green/glom/glom.go:34.64,36.36 2 1
+red-green/glom/glom.go:42.2,42.70 1 1
+red-green/glom/glom.go:45.2,45.41 1 1
+red-green/glom/glom.go:49.2,52.30 3 1
+red-green/glom/glom.go:57.2,57.20 1 1
+red-green/glom/glom.go:36.36,37.22 1 1
+red-green/glom/glom.go:38.22,39.36 1 1
+red-green/glom/glom.go:42.70,44.3 1 1
+red-green/glom/glom.go:45.41,47.3 1 1
+red-green/glom/glom.go:52.30,55.3 1 1
+red-green/glom/glom.go:60.45,64.37 2 1
+red-green/glom/glom.go:79.2,79.15 1 1
+red-green/glom/glom.go:65.19,68.26 3 1
+red-green/glom/glom.go:71.36,73.43 2 1
+red-green/glom/glom.go:76.22,77.31 1 1
+red-green/glom/glom.go:68.26,70.4 1 1
+red-green/glom/glom.go:73.43,75.4 1 1
+red-green/glom/glom.go:82.52,83.31 1 1
+red-green/glom/glom.go:88.2,88.14 1 1
+red-green/glom/glom.go:83.31,84.20 1 1
+red-green/glom/glom.go:84.20,86.4 1 1
+red-green/glom/glom.go:91.79,92.47 1 1
+red-green/glom/glom.go:117.2,117.117 1 1
+red-green/glom/glom.go:92.47,94.47 1 1
+red-green/glom/glom.go:95.20,97.18 2 1
+red-green/glom/glom.go:100.4,100.25 1 1
+red-green/glom/glom.go:101.37,103.18 2 1
+red-green/glom/glom.go:112.23,114.54 2 1
+red-green/glom/glom.go:97.18,99.5 1 0
+red-green/glom/glom.go:103.18,105.19 2 1
+red-green/glom/glom.go:108.5,108.24 1 1
+red-green/glom/glom.go:105.19,107.6 1 0
+red-green/glom/glom.go:109.10,111.5 1 0
+red-green/glom/glom.go:120.48,122.31 2 1
+red-green/glom/glom.go:125.2,125.15 1 1
+red-green/glom/glom.go:122.31,124.3 1 1
+red-green/glom/glom.go:128.63,134.36 5 1
+red-green/glom/glom.go:154.2,154.23 1 1
+red-green/glom/glom.go:134.36,137.57 1 1
+red-green/glom/glom.go:137.57,139.4 1 1
+red-green/glom/glom.go:139.9,140.18 1 1
+red-green/glom/glom.go:140.18,142.19 2 1
+red-green/glom/glom.go:142.19,145.6 1 0
+red-green/glom/glom.go:145.11,148.6 2 1
+red-green/glom/glom.go:149.10,151.5 1 1

+ 155 - 0
glom/glom.go

@@ -0,0 +1,155 @@
+package glom
+
+import (
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+
+	"github.com/fatih/structs"
+)
+
+// Based on sliceToInterface
+func mapToInterface(data interface{}) (map[string]interface{}, error) {
+	mapV := reflect.ValueOf(data)
+	if mapV.Kind() != reflect.Map {
+		return nil, fmt.Errorf("Failed to convert %v, given %v type to map[string]interface{}", mapV, reflect.TypeOf(data))
+	}
+	if mapV.IsNil() || !mapV.IsValid() {
+		return nil, fmt.Errorf("Given nil or empty map!")
+	}
+
+	result := make(map[string]interface{})
+	keys := mapV.MapKeys()
+	for k := range keys {
+		//fmt.Printf("%d/%d = %v", k, len(keys), mapV.MapIndex(keys[k]))
+		result[keys[k].String()] = mapV.MapIndex(keys[k]).Interface()
+	}
+
+	return result, nil
+}
+
+// https://gist.github.com/heri16/077282d46ae95d48d430a90fb6accdff
+// I only need the length
+func sliceToInterface(data interface{}) ([]interface{}, error) {
+	sliceV := reflect.ValueOf(data)
+	if sliceV.Kind() == reflect.Slice { // Prevent us from converting an interface to interface
+		switch data.(type) {
+		case []interface{}:
+			return data.([]interface{}), nil
+		}
+	}
+	if sliceV.Kind() != reflect.Slice && sliceV.Kind() != reflect.Array {
+		return nil, fmt.Errorf("Failed to convert %v, given %v type to []interface{}", sliceV, reflect.TypeOf(data))
+	}
+	if sliceV.IsNil() || !sliceV.IsValid() {
+		return nil, fmt.Errorf("Given nil or empty slice!")
+	}
+
+	length := sliceV.Len()
+	result := make([]interface{}, length)
+
+	for i := 0; i < length; i++ {
+		//fmt.Printf("%d/%d = %v\r\n", i, length-1, sliceV.Index(i))
+		result[i] = sliceV.Index(i).Interface()
+	}
+
+	return result, nil
+}
+
+func getPossible(data interface{}) []string {
+	var result []string
+	//fmt.Printf("%v (%v)\r\n", reflect.TypeOf(data).Kind(), reflect.TypeOf(data))
+	//fmt.Println(data)
+	switch reflect.TypeOf(data).Kind() {
+	case reflect.Map:
+		mapV := reflect.ValueOf(data)
+		keysV := mapV.MapKeys()
+		for key := range keysV {
+			result = append(result, keysV[key].String())
+		}
+	case reflect.Array, reflect.Slice:
+		sliceV := reflect.ValueOf(data)
+		for idx := 0; idx < sliceV.Len(); idx++ {
+			result = append(result, fmt.Sprintf("%d", idx))
+		}
+	case reflect.Struct:
+		result = structs.Names(data)
+	}
+	return result
+}
+
+func inside(possible []string, target string) bool {
+	for _, val := range possible {
+		if target == val {
+			return true
+		}
+	}
+	return false
+}
+
+func next_level(current_level interface{}, go_to string) (interface{}, error) {
+	if inside(getPossible(current_level), go_to) {
+		//fmt.Printf("%v (%v)\r\n", reflect.TypeOf(current_level).Kind(), reflect.TypeOf(current_level))
+		switch reflect.TypeOf(current_level).Kind() {
+		case reflect.Map:
+			CL, err := mapToInterface(current_level)
+			if err != nil {
+				return nil, err
+			}
+			return CL[go_to], nil
+		case reflect.Array, reflect.Slice:
+			val, err := strconv.Atoi(go_to)
+			if err == nil {
+				CL, err := sliceToInterface(current_level)
+				if err != nil {
+					return nil, err
+				}
+				return CL[val], nil
+			} else {
+				return nil, err
+			}
+		case reflect.Struct:
+			structV := reflect.ValueOf(current_level)
+			return structV.FieldByName(go_to).Interface(), nil
+		}
+	}
+	return nil, fmt.Errorf("Failed moving to '%s' from '%s' (%v)", go_to, current_level, reflect.TypeOf(current_level))
+}
+
+func list_possible(possible []string) []string {
+	var result []string
+	for _, val := range possible {
+		result = append(result, fmt.Sprintf("'%s'", val))
+	}
+	return result
+}
+
+func Glom(data interface{}, path string) (interface{}, error) {
+	complete_path := strings.Split(path, ".")
+	//fmt.Printf("Seeking '%s' will take %d steps\r\n", path, len(complete_path))
+	var path_taken []string
+	var currently interface{}
+	currently = data
+	for _, hop := range complete_path {
+		//fmt.Printf("current: %v\r\n", currently)
+		//fmt.Printf("Path: '%v'\r\n", strings.Join(path_taken, "."))
+		if hop != "*" && !inside(getPossible(currently), hop) {
+			return nil, fmt.Errorf("Failed moving to '%s' from path of '%s', options are %s (%d)", hop, strings.Join(path_taken, "."), strings.Join(list_possible(getPossible(currently)), ", "), len(getPossible(currently)))
+		} else {
+			if hop != "*" {
+				next, err := next_level(currently, hop)
+				if err != nil {
+					//return nil, fmt.Errorf("Failed moving to '%s' from path of '%s', options are %s (%d)", hop, strings.Join(path_taken, "."), strings.Join(list_possible(getPossible(next)), ", "), len(getPossible(next)))
+					return nil, err
+				} else {
+					path_taken = append(path_taken, hop)
+					currently = next
+				}
+			} else {
+				return currently, nil
+			}
+		}
+	}
+	return currently, nil
+}

+ 247 - 0
glom/glom_test.go

@@ -0,0 +1,247 @@
+package glom
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+)
+
+func TestGlomArray(t *testing.T) {
+	var data []interface{}
+	data = append(data, "Goose")
+
+	test1 := make(map[string]interface{})
+
+	test1a := make(map[string]interface{})
+	test1a["name"] = "Ducky"
+	test1a["age"] = 62
+	test1a["race"] = "Duck"
+
+	test1b := make(map[string]interface{})
+	test1b["name"] = "Sir Meow"
+	test1b["age"] = 12
+	test1b["race"] = "Cat"
+
+	var animals []interface{}
+	animals = append(animals, test1a)
+	animals = append(animals, test1b)
+
+	test1["animals"] = animals
+	data = append(data, test1)
+
+	/*
+		HJSON Representation of data
+		data = [
+			"Goose"
+			{
+				"animals": [
+					{"name": "Ducky", "age": 62, "race": "Duck"}
+					{"name": "Sir Meow", "age": 12, "race": "Cat"}
+				]
+			}
+		]
+	*/
+	test, err := Glom(data, "1.animals.1.name")
+	if err != nil {
+		t.Errorf("Unexpected Error: \"%v\"", err)
+	} else if test != "Sir Meow" {
+		t.Errorf("Failed getting 'Sir Meow' got \"%v\"", test)
+	}
+}
+
+func TestGetPossible(t *testing.T) {
+	var data []string
+	data = append(data, "One")
+	data = append(data, "Two")
+	data = append(data, "Three")
+	data = append(data, "Four")
+
+	result := getPossible(data)
+	if len(result) != len(data) {
+		t.Errorf("Expected even size, %d != %d", len(result), len(data))
+	}
+}
+
+func TestStruct(t *testing.T) {
+	type Animal struct {
+		Name     string
+		Lifespan int
+	}
+
+	cat := Animal{"Cat", 12}
+	dog := Animal{"Dog", 13}
+
+	var data []Animal
+	data = append(data, cat)
+	data = append(data, dog)
+
+	test, err := Glom(data, "1.*")
+	if err != nil {
+		t.Errorf("TestStruct 1/3: Unexpected Error: \"%v\"", err)
+	} else {
+		if fmt.Sprintf("%v", test) != fmt.Sprintf("%v", dog) {
+			t.Errorf("TestStruct 1/3: Failed getting '%v' got '%v'", dog, test)
+		}
+	}
+
+	test2, err2 := Glom(cat, "Lifespan")
+	if err2 != nil {
+		t.Errorf("TestStruct 2/3: Unexpected Error: \"%v\"", err2)
+	} else {
+		if test2 != cat.Lifespan {
+			t.Errorf("TestStruct 2/3: Failed getting '%v' got '%v'", cat.Lifespan, test2)
+		}
+	}
+
+	data2 := make(map[string]Animal)
+
+	data2["Squirrel"] = Animal{"Squirrel", 999}
+	data2["Hamster"] = Animal{"Hamster", 4}
+
+	test3, err3 := Glom(data2, "Squirrel.Name")
+	if err3 != nil {
+		t.Errorf("TestStruct 3/3: Unexpected Error: \"%v\"", err3)
+	} else {
+		if test3 != "Squirrel" {
+			t.Errorf("TestStruct 3/3: Failed getting 'Squirrel' got '%v'", test3)
+		}
+	}
+}
+
+func TestListPossible(t *testing.T) {
+	var list []string
+	list = append(list, "One")
+	list = append(list, "Two")
+	list = append(list, "Three")
+
+	result := list_possible(list)
+
+	if strings.Join(result, ", ") != "'One', 'Two', 'Three'" {
+		t.Errorf("Failed getting \"%s\" got \"%v\"", "'One', 'Two', 'Three'", strings.Join(result, ", "))
+	}
+}
+
+func TestFail(t *testing.T) {
+	data := make(map[string]interface{})
+	data["Duck"] = "Quack"
+	data["Cheese"] = 3
+	data["Mouse"] = true
+
+	test, err := Glom(data, "Moose")
+	if err == nil {
+		t.Errorf("Expected Error, got '%v'", test)
+	}
+}
+
+func TestMapToInter(t *testing.T) {
+	m := make(map[string]string)
+	m["Duck"] = "Quack"
+	m["Cheese"] = "Yes Please!"
+	m["Mouse"] = "true"
+	var s []string
+	s = append(s, "Duck")
+	s = append(s, "Cheese")
+	s = append(s, "Mouse")
+	var m2 map[string]int
+
+	_, err1 := mapToInterface(m)
+	if err1 != nil {
+		t.Errorf("Unexpected Error given map: %v", err1)
+	}
+
+	test2, err2 := mapToInterface(s)
+	if err2 == nil {
+		t.Errorf("Expected Error given slice, got '%v'", test2)
+	}
+
+	test3, err3 := mapToInterface(m2)
+	if err3 == nil {
+		t.Errorf("Expected Error given invalid/empty map, got '%v'", test3)
+	}
+}
+
+func TestSliceToInter(t *testing.T) {
+	m := make(map[string]string)
+	m["Duck"] = "Quack"
+	m["Cheese"] = "Yes Please!"
+	m["Mouse"] = "true"
+	var s []string
+	s = append(s, "Duck")
+	s = append(s, "Cheese")
+	s = append(s, "Mouse")
+	var m2 map[string]int
+	var s2 []int
+
+	test1, err1 := sliceToInterface(m)
+	if err1 == nil {
+		t.Errorf("Expected Error given map, got '%v'", test1)
+	}
+
+	_, err2 := sliceToInterface(s)
+	if err2 != nil {
+		t.Errorf("Unexpected Error given slice: %v", err2)
+	}
+
+	test3, err3 := sliceToInterface(m2)
+	if err3 == nil {
+		t.Errorf("Expected Error given invalid/empty map, got '%v'", test3)
+	}
+
+	test4, err4 := sliceToInterface(s2)
+	if err4 == nil {
+		t.Errorf("Expected Error given invalid/empty slice, got '%v'", test4)
+	}
+}
+
+func TestEdgeCasesMapNextLvl(t *testing.T) {
+	// This doesn't work, I thought it would but it does not
+	var m map[string]int
+	m2 := make(map[string]int)
+	m2["Cheese"] = 6
+	m2["C"] = 1
+	m2["h"] = 1
+	m2["e"] = 3
+	m2["s"] = 1
+
+	test1, err1 := next_level(m, "failwhale")
+	if err1 == nil {
+		t.Errorf("Expected Error given invalid/empty map, got '%v'", test1)
+	}
+
+	test2, err2 := next_level(m2, "n")
+	if err2 == nil {
+		t.Errorf("Expected Error given map but invalid key, got '%v'", test2)
+	}
+}
+
+func TestEdgeCasesGlom(t *testing.T) {
+	// This is just a generic test, nothing fancy
+	data := make(map[string]interface{})
+
+	lvl2 := make(map[string]interface{})
+	lvl2["Duck"] = "Quack"
+	lvl2["Cheese"] = 6
+	lvl2["Mouse"] = true
+	data["part1"] = lvl2
+
+	var lvl1 []interface{}
+	lvl1 = append(lvl1, "Pig")
+	lvl1 = append(lvl1, "Chicken")
+	lvl1 = append(lvl1, "Cow")
+	lvl1 = append(lvl1, "Dog")
+	lvl1 = append(lvl1, "Cat")
+	lvl1 = append(lvl1, "Horse")
+	lvl1 = append(lvl1, true)
+	lvl1 = append(lvl1, 42)
+	data["part2"] = lvl1
+
+	_, err1 := Glom(data, "part1.Mouse")
+	if err1 != nil {
+		t.Errorf("Unexpected Error (part1.Mouse = true): %v", err1)
+	}
+
+	_, err2 := Glom(data, "part2.3")
+	if err2 != nil {
+		t.Errorf("Unexpected Error (part2.3 = 'Dog'): %v", err2)
+	}
+}

+ 6 - 0
glom/go.mod

@@ -0,0 +1,6 @@
+module red-green/glom
+
+go 1.17
+
+require github.com/fatih/structs v1.1.0 // indirect
+

+ 2 - 0
glom/go.sum

@@ -0,0 +1,2 @@
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=