diff --git a/Gopkg.lock b/Gopkg.lock
index e04a8138adba31b534e9cdef85748ef8823be2f1..fcf2be127dcf50bcdc6877b7b3cbc0c25e294106 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -255,6 +255,18 @@
   packages = ["."]
   revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe"
 
+[[projects]]
+  name = "github.com/mattn/go-runewidth"
+  packages = ["."]
+  revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
+  version = "v0.0.2"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/olekukonko/tablewriter"
+  packages = ["."]
+  revision = "b8a9be070da40449e501c3c4730a889e42d87a9e"
+
 [[projects]]
   name = "github.com/onsi/ginkgo"
   packages = [
@@ -359,10 +371,17 @@
   packages = ["."]
   revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
 
+[[projects]]
+  name = "github.com/stretchr/objx"
+  packages = ["."]
+  revision = "facf9a85c22f48d2f52f2380e4efce1768749a89"
+  version = "v0.1"
+
 [[projects]]
   name = "github.com/stretchr/testify"
   packages = [
     "assert",
+    "mock",
     "require"
   ]
   revision = "f6abca593680b2315d2075e0f5e2a9751e3f431a"
@@ -603,6 +622,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "8b786abd404b80a5933fdf9b29cae67837bf6630041f144bffd7092fdb0e331c"
+  inputs-digest = "cb1ea2fcb93a4e383a77524868cca62997ccf5e8b862f2d8250c388870c4da49"
   solver-name = "gps-cdcl"
   solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
index 5899b546977d044a16cb174df1a336d2eecf3422..ff1279c65a4b43f967b447cbc84198281226c488 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -32,10 +32,6 @@
   name = "github.com/emicklei/go-restful-swagger12"
   revision = "7524189396c68dc4b04d53852f9edc00f816b123"
 
-[[constraint]]
-  name = "github.com/fatih/color"
-  revision = "5df930a27be2502f99b292b7cc09ebad4d0891f4"
-
 [[constraint]]
   name = "github.com/ghodss/yaml"
   revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
diff --git a/Makefile b/Makefile
index a9e3d03a36df44075a05f1dd304653f6b342c6c8..431ea2248c5ba1d38c6ff5d6c016c1e96e0e6bb6 100644
--- a/Makefile
+++ b/Makefile
@@ -17,10 +17,11 @@ VERSION?=dev-$(shell date +%FT%T%z)
 KS_BIN?=ks
 
 APIMACHINERY_VER := $(shell dep status | grep k8s.io/apimachinery | awk '{print $$3}')
+REVISION=$(shell git rev-parse HEAD)
 
 GO = go
 EXTRA_GO_FLAGS =
-GO_FLAGS = -ldflags="-X main.version=$(VERSION) -X main.apimachineryVersion=$(APIMACHINERY_VER) $(GO_LDFLAGS)" $(EXTRA_GO_FLAGS)
+GO_FLAGS = -ldflags="-X main.version=$(VERSION) -X main.apimachineryVersion=$(APIMACHINERY_VER) -X generator.revision=$(REVISION) $(GO_LDFLAGS) " $(EXTRA_GO_FLAGS)
 GOFMT = gofmt
 # GINKGO = "go test" also works if you want to avoid ginkgo tool
 GINKGO = ginkgo
diff --git a/actions/upgrade.go b/actions/upgrade.go
new file mode 100644
index 0000000000000000000000000000000000000000..52d54e9dce32b26ca47bfc4fe641196eb718a0a9
--- /dev/null
+++ b/actions/upgrade.go
@@ -0,0 +1,27 @@
+package actions
+
+import (
+	"os"
+
+	"github.com/ksonnet/ksonnet/metadata"
+)
+
+// Upgrade upgrades a ksonnet application.
+func Upgrade(dryRun bool) error {
+	cwd, err := os.Getwd()
+	if err != nil {
+		return err
+	}
+
+	m, err := metadata.Find(cwd)
+	if err != nil {
+		return err
+	}
+
+	a, err := m.App()
+	if err != nil {
+		return err
+	}
+
+	return a.Upgrade(dryRun)
+}
diff --git a/client/client.go b/client/client.go
index f7739d36b91b15b979b5b4444e23b946027e3e2c..5561f70a77189063aad7c7ada51abb17cd4b72b0 100644
--- a/client/client.go
+++ b/client/client.go
@@ -238,12 +238,12 @@ func (c *Config) overrideCluster(envName string) error {
 	//
 
 	log.Debugf("Validating deployment at '%s' with server '%v'", envName, reflect.ValueOf(servers).MapKeys())
-	env, err := metadataManager.GetEnvironment(envName)
+	destination, err := metadataManager.GetDestination(envName)
 	if err != nil {
 		return err
 	}
 
-	server, err := str.NormalizeURL(env.Destination.Server)
+	server, err := str.NormalizeURL(destination.Server())
 	if err != nil {
 		return err
 	}
@@ -255,11 +255,12 @@ func (c *Config) overrideCluster(envName string) error {
 			c.Overrides.Context.Cluster = clusterName
 		}
 		if c.Overrides.Context.Namespace == "" {
-			log.Debugf("Overwriting --namespace flag with '%s'", env.Destination.Namespace)
-			c.Overrides.Context.Namespace = env.Destination.Namespace
+			log.Debugf("Overwriting --namespace flag with '%s'", destination.Namespace())
+			c.Overrides.Context.Namespace = destination.Namespace()
 		}
 		return nil
 	}
 
-	return fmt.Errorf("Attempting to deploy to environment '%s' at '%s', but cannot locate a server at that address", envName, env.Destination.Server)
+	return fmt.Errorf("Attempting to deploy to environment '%s' at '%s', but cannot locate a server at that address",
+		envName, destination.Server())
 }
diff --git a/cmd/param.go b/cmd/param.go
index 40b4fc046fc4909b9b5648c829e25cca17775ac5..fbb269181e20f981ccf2f629b5fe8fde02e0b9db 100644
--- a/cmd/param.go
+++ b/cmd/param.go
@@ -17,6 +17,7 @@ package cmd
 
 import (
 	"fmt"
+	"os"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -27,6 +28,7 @@ import (
 const (
 	flagParamEnv       = "env"
 	flagParamComponent = "component"
+	flagParamNamespace = "namespace"
 )
 
 var paramShortDesc = map[string]string{
@@ -44,6 +46,7 @@ func init() {
 
 	paramSetCmd.PersistentFlags().String(flagParamEnv, "", "Specify environment to set parameters for")
 	paramListCmd.PersistentFlags().String(flagParamEnv, "", "Specify environment to list parameters for")
+	paramListCmd.Flags().String(flagParamNamespace, "", "Specify namespace to list parameters for")
 	paramDiffCmd.PersistentFlags().String(flagParamComponent, "", "Specify the component to diff against")
 }
 
@@ -156,7 +159,12 @@ var paramListCmd = &cobra.Command{
 			return err
 		}
 
-		c := kubecfg.NewParamListCmd(component, env)
+		nsName, err := flags.GetString(flagParamNamespace)
+		if err != nil {
+			return err
+		}
+
+		c := kubecfg.NewParamListCmd(component, env, nsName)
 
 		return c.Run(cmd.OutOrStdout())
 	},
@@ -196,6 +204,11 @@ var paramDiffCmd = &cobra.Command{
 			return fmt.Errorf("'param diff' takes exactly two arguments: the respective names of the environments being diffed")
 		}
 
+		cwd, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+
 		env1 := args[0]
 		env2 := args[1]
 
@@ -204,7 +217,7 @@ var paramDiffCmd = &cobra.Command{
 			return err
 		}
 
-		c := kubecfg.NewParamDiffCmd(env1, env2, component)
+		c := kubecfg.NewParamDiffCmd(appFs, cwd, env1, env2, component)
 
 		return c.Run(cmd.OutOrStdout())
 	},
diff --git a/cmd/pkg.go b/cmd/pkg.go
index 68728cde2d42dd456c29ee3392f071f9660eb392..59b297780460ea5abe564f515dbc662dd72e8439 100644
--- a/cmd/pkg.go
+++ b/cmd/pkg.go
@@ -242,7 +242,7 @@ var pkgListCmd = &cobra.Command{
 			return err
 		}
 
-		app, err := manager.AppSpec()
+		app, err := manager.App()
 		if err != nil {
 			return err
 		}
@@ -254,14 +254,14 @@ var pkgListCmd = &cobra.Command{
 				strings.Repeat("=", len(nameHeader)),
 				strings.Repeat("=", len(installedHeader))},
 		}
-		for name := range app.Registries {
+		for name := range app.Registries() {
 			reg, _, err := manager.GetRegistry(name)
 			if err != nil {
 				return err
 			}
 
 			for libName := range reg.Libraries {
-				_, isInstalled := app.Libraries[libName]
+				_, isInstalled := app.Libraries()[libName]
 				if isInstalled {
 					rows = append(rows, []string{name, libName, installed})
 				} else {
diff --git a/cmd/registry.go b/cmd/registry.go
index 7d385272843871ad61d37080428d217b85b508a7..a3af7786e4d7f88c03562c7d998856cca45d9221 100644
--- a/cmd/registry.go
+++ b/cmd/registry.go
@@ -82,7 +82,7 @@ var registryListCmd = &cobra.Command{
 			return err
 		}
 
-		app, err := manager.AppSpec()
+		app, err := manager.App()
 		if err != nil {
 			return err
 		}
@@ -95,7 +95,7 @@ var registryListCmd = &cobra.Command{
 				strings.Repeat("=", len(uriHeader)),
 			},
 		}
-		for name, regRef := range app.Registries {
+		for name, regRef := range app.Registries() {
 			rows = append(rows, []string{name, regRef.Protocol, regRef.URI})
 		}
 
@@ -141,12 +141,12 @@ var registryDescribeCmd = &cobra.Command{
 			return err
 		}
 
-		app, err := manager.AppSpec()
+		app, err := manager.App()
 		if err != nil {
 			return err
 		}
 
-		regRef, exists := app.GetRegistryRef(name)
+		regRef, exists := app.Registries()[name]
 		if !exists {
 			return fmt.Errorf("Registry '%s' doesn't exist", name)
 		}
diff --git a/cmd/root.go b/cmd/root.go
index 9b22ec396eede149ea0faad1c0df311e7903b160..3151d1468e97373a085952ae4843f86883490218 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -29,6 +29,7 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 
 	"github.com/ksonnet/ksonnet/component"
+	"github.com/ksonnet/ksonnet/env"
 	"github.com/ksonnet/ksonnet/metadata"
 	"github.com/ksonnet/ksonnet/plugin"
 	str "github.com/ksonnet/ksonnet/strings"
@@ -326,9 +327,14 @@ func (te *cmdObjExpander) Expand() ([]*unstructured.Unstructured, error) {
 		return nil, err
 	}
 
+	app, err := manager.App()
+	if err != nil {
+		return nil, err
+	}
+
 	expander.FlagJpath = append([]string{string(vendorPath), string(libPath), string(envPath)}, expander.FlagJpath...)
 
-	namespacedComponentPaths, err := component.MakePathsByNamespace(te.config.fs, manager, te.config.cwd, te.config.env)
+	namespacedComponentPaths, err := component.MakePathsByNamespace(te.config.fs, app, te.config.cwd, te.config.env)
 	if err != nil {
 		return nil, errors.Wrap(err, "component paths")
 	}
@@ -453,28 +459,20 @@ func importParams(path string) string {
 	return fmt.Sprintf(`%s=import "%s"`, metadata.ParamsExtCodeKey, path)
 }
 
-func importEnv(manager metadata.Manager, env string) (string, error) {
-	app, err := manager.AppSpec()
+func importEnv(manager metadata.Manager, envName string) (string, error) {
+	app, err := manager.App()
 	if err != nil {
 		return "", err
 	}
 
-	spec, exists := app.GetEnvironmentSpec(env)
-	if !exists {
-		return "", fmt.Errorf("Environment '%s' does not exist in app.yaml", env)
-	}
-
-	type EnvironmentSpec struct {
-		Server    string `json:"server"`
-		Namespace string `json:"namespace"`
+	spec, err := app.Environment(envName)
+	if err != nil {
+		return "", fmt.Errorf("Environment '%s' does not exist in app.yaml", envName)
 	}
 
-	toMarshal := &EnvironmentSpec{
-		Server:    spec.Destination.Server,
-		Namespace: spec.Destination.Namespace,
-	}
+	destination := env.NewDestination(spec.Destination.Server, spec.Destination.Namespace)
 
-	marshalled, err := json.Marshal(toMarshal)
+	marshalled, err := json.Marshal(&destination)
 	if err != nil {
 		return "", err
 	}
diff --git a/cmd/upgrade.go b/cmd/upgrade.go
new file mode 100644
index 0000000000000000000000000000000000000000..eeaada4488622d4f5b6999e5df4f15e911c1b596
--- /dev/null
+++ b/cmd/upgrade.go
@@ -0,0 +1,62 @@
+// Copyright 2018 The ksonnet authors
+//
+//
+//    Licensed under the Apache License, Version 2.0 (the "License");
+//    you may not use this file except in compliance with the License.
+//    You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//    Unless required by applicable law or agreed to in writing, software
+//    distributed under the License is distributed on an "AS IS" BASIS,
+//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//    See the License for the specific language governing permissions and
+//    limitations under the License.
+
+package cmd
+
+import (
+	"github.com/ksonnet/ksonnet/actions"
+	"github.com/spf13/cobra"
+)
+
+const (
+	upgradeShortDesc  = "Upgrade ks configuration"
+	flagUpgradeDryRun = "dry-run"
+)
+
+var upgradeCmd = &cobra.Command{
+	Use:   "upgrade [--dry-run]",
+	Short: upgradeShortDesc,
+	Long:  upgradeLong,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		dryRun, err := cmd.Flags().GetBool(flagUpgradeDryRun)
+		if err != nil {
+			return err
+		}
+
+		return actions.Upgrade(dryRun)
+	},
+}
+
+func init() {
+	RootCmd.AddCommand(upgradeCmd)
+
+	upgradeCmd.Flags().Bool(flagUpgradeDryRun, false, "Dry-run upgrade process. Prints out changes.")
+}
+
+const upgradeLong = `
+The upgrade command upgrades a ksonnet application to the latest version.
+
+### Syntax
+
+	Example:
+
+# Upgrade ksonnet application in dry-run mode to see the changes to be performed by the
+# upgrade process.
+ks upgrade --dry-run
+
+# Upgrade ksonnet application. This will update app.yaml to apiVersion 0.1.0
+# and migrate environment spec.json files to ` + "`" + `app.yaml` + "`" + `.
+ks upgrade
+`
diff --git a/component/component.go b/component/component.go
index 4c3f6e5e2df1db789f459d18261321e25ba9f654..df0db014e2f163218c432cc5076d14b8907770cc 100644
--- a/component/component.go
+++ b/component/component.go
@@ -77,6 +77,15 @@ type Namespace struct {
 	fs   afero.Fs
 }
 
+// NewNamespace creates an an instance of Namespace.
+func NewNamespace(fs afero.Fs, root, name string) Namespace {
+	return Namespace{
+		Path: name,
+		root: root,
+		fs:   fs,
+	}
+}
+
 // ExtractNamespacedComponent extracts a namespace and a component from a path.
 func ExtractNamespacedComponent(fs afero.Fs, root, path string) (Namespace, string) {
 	path, component := filepath.Split(path)
@@ -197,15 +206,9 @@ func isComponentDir(fs afero.Fs, path string) (bool, error) {
 	return false, nil
 }
 
-// AppSpecer is implemented by any value that has a AppSpec method. The AppSpec method is
-// used to retrieve a ksonnet AppSpec.
-type AppSpecer interface {
-	AppSpec() (*app.Spec, error)
-}
-
 // MakePathsByNamespace creates a map of component paths categorized by namespace.
-func MakePathsByNamespace(fs afero.Fs, appSpecer AppSpecer, root, env string) (map[Namespace][]string, error) {
-	paths, err := MakePaths(fs, appSpecer, root, env)
+func MakePathsByNamespace(fs afero.Fs, ksApp app.App, root, env string) (map[Namespace][]string, error) {
+	paths, err := MakePaths(fs, ksApp, root, env)
 	if err != nil {
 		return nil, err
 	}
@@ -230,8 +233,8 @@ func MakePathsByNamespace(fs afero.Fs, appSpecer AppSpecer, root, env string) (m
 }
 
 // MakePaths creates a slice of component paths
-func MakePaths(fs afero.Fs, appSpecer AppSpecer, root, env string) ([]string, error) {
-	cpl, err := newComponentPathLocator(fs, appSpecer, env)
+func MakePaths(fs afero.Fs, ksApp app.App, root, env string) ([]string, error) {
+	cpl, err := newComponentPathLocator(fs, ksApp, env)
 	if err != nil {
 		return nil, errors.Wrap(err, "create component path locator")
 	}
@@ -244,22 +247,17 @@ type componentPathLocator struct {
 	envSpec *app.EnvironmentSpec
 }
 
-func newComponentPathLocator(fs afero.Fs, appSpecer AppSpecer, env string) (*componentPathLocator, error) {
-	if appSpecer == nil {
-		return nil, errors.New("appSpecer is nil")
+func newComponentPathLocator(fs afero.Fs, ksApp app.App, env string) (*componentPathLocator, error) {
+	if ksApp == nil {
+		return nil, errors.New("app is nil")
 	}
 
 	if fs == nil {
 		return nil, errors.New("fs is nil")
 	}
 
-	appSpec, err := appSpecer.AppSpec()
+	envSpec, err := ksApp.Environment(env)
 	if err != nil {
-		return nil, errors.Wrap(err, "lookup application spec")
-	}
-
-	envSpec, ok := appSpec.GetEnvironmentSpec(env)
-	if !ok {
 		return nil, errors.Errorf("can't find %s environment", env)
 	}
 
diff --git a/component/component_test.go b/component/component_test.go
index 30ee8fde755ff3e002610702862433aa4103a3b7..c7dfae0fed857b3abc801140330b53f139828adf 100644
--- a/component/component_test.go
+++ b/component/component_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/ksonnet/ksonnet/metadata/app/mocks"
 )
 
 var (
@@ -48,21 +49,6 @@ var (
 	}
 )
 
-type stubAppSpecer struct {
-	appSpec *app.Spec
-	err     error
-}
-
-var _ AppSpecer = (*stubAppSpecer)(nil)
-
-func newStubAppSpecer(appSpec *app.Spec) *stubAppSpecer {
-	return &stubAppSpecer{appSpec: appSpec}
-}
-
-func (s *stubAppSpecer) AppSpec() (*app.Spec, error) {
-	return s.appSpec, s.err
-}
-
 func makePaths(t *testing.T, fs afero.Fs, paths []string) {
 	for _, path := range paths {
 		dir := filepath.Dir(path)
@@ -311,15 +297,13 @@ func TestMakePathsByNameSpace(t *testing.T) {
 				Targets: tc.targets,
 			}
 
-			appSpec := &app.Spec{
-				Environments: app.EnvironmentSpecs{"default": envSpec},
-			}
-			appSpecer := newStubAppSpecer(appSpec)
+			appMock := &mocks.App{}
+			appMock.On("Environment", "default").Return(envSpec, nil)
 
 			root := "/"
 			env := "default"
 
-			paths, err := MakePathsByNamespace(fs, appSpecer, root, env)
+			paths, err := MakePathsByNamespace(fs, appMock, root, env)
 			if tc.isErr {
 				require.Error(t, err)
 			} else {
@@ -394,15 +378,13 @@ func TestMakePaths(t *testing.T) {
 				Targets: tc.targets,
 			}
 
-			appSpec := &app.Spec{
-				Environments: app.EnvironmentSpecs{"default": envSpec},
-			}
-			appSpecer := newStubAppSpecer(appSpec)
+			appMock := &mocks.App{}
+			appMock.On("Environment", "default").Return(envSpec, nil)
 
 			root := "/"
 			env := "default"
 
-			paths, err := MakePaths(fs, appSpecer, root, env)
+			paths, err := MakePaths(fs, appMock, root, env)
 			if tc.isErr {
 				require.Error(t, err)
 			} else {
@@ -413,14 +395,14 @@ func TestMakePaths(t *testing.T) {
 	}
 }
 
-func TestMakePaths_invalid_appSpecer(t *testing.T) {
+func TestMakePaths_invalid_app(t *testing.T) {
 	fs := afero.NewMemMapFs()
 	_, err := MakePaths(fs, nil, "/", "default")
 	require.Error(t, err)
 }
 
 func TestMakePaths_invalid_fs(t *testing.T) {
-	appSpecer := newStubAppSpecer(nil)
-	_, err := MakePaths(nil, appSpecer, "/", "default")
+	appMock := &mocks.App{}
+	_, err := MakePaths(nil, appMock, "/", "default")
 	require.Error(t, err)
 }
diff --git a/docs/cli-reference/ks.md b/docs/cli-reference/ks.md
index 041bc8b554dceb9dc261dfd590e3858fc6410b5d..3315d202fbc823bf6bd1e78628f6f4af5fd2d3f4 100644
--- a/docs/cli-reference/ks.md
+++ b/docs/cli-reference/ks.md
@@ -36,6 +36,7 @@ ks [flags]
 * [ks prototype](ks_prototype.md)	 - Instantiate, inspect, and get examples for ksonnet prototypes
 * [ks registry](ks_registry.md)	 - Manage registries for current project
 * [ks show](ks_show.md)	 - Show expanded manifests for a specific environment.
+* [ks upgrade](ks_upgrade.md)	 - Upgrade ks configuration
 * [ks validate](ks_validate.md)	 - Check generated component manifests against the server's API
 * [ks version](ks_version.md)	 - Print version information for this ksonnet binary
 
diff --git a/docs/cli-reference/ks_param_list.md b/docs/cli-reference/ks_param_list.md
index 38b12d1ad91a24a226663f8b7d189accfa46348f..390980ed6d84ae986c432263889619655dea7e4b 100644
--- a/docs/cli-reference/ks_param_list.md
+++ b/docs/cli-reference/ks_param_list.md
@@ -42,8 +42,9 @@ ks param list guestbook --env=dev
 ### Options
 
 ```
-      --env string   Specify environment to list parameters for
-  -h, --help         help for list
+      --env string         Specify environment to list parameters for
+  -h, --help               help for list
+      --namespace string   Specify namespace to list parameters for
 ```
 
 ### Options inherited from parent commands
diff --git a/docs/cli-reference/ks_upgrade.md b/docs/cli-reference/ks_upgrade.md
new file mode 100644
index 0000000000000000000000000000000000000000..a3d8202e39493c40877876454d7555372544cc4c
--- /dev/null
+++ b/docs/cli-reference/ks_upgrade.md
@@ -0,0 +1,43 @@
+## ks upgrade
+
+Upgrade ks configuration
+
+### Synopsis
+
+
+The upgrade command upgrades a ksonnet application to the latest version.
+
+### Syntax
+
+	Example:
+
+# Upgrade ksonnet application in dry-run mode to see the changes to be performed by the
+# upgrade process.
+ks upgrade --dry-run
+
+# Upgrade ksonnet application. This will update app.yaml to apiVersion 0.1.0
+# and migrate environment spec.json files to `app.yaml`.
+ks upgrade
+
+
+```
+ks upgrade [--dry-run] [flags]
+```
+
+### Options
+
+```
+      --dry-run   Dry-run upgrade process. Prints out changes.
+  -h, --help      help for upgrade
+```
+
+### Options inherited from parent commands
+
+```
+  -v, --verbose count[=-1]   Increase verbosity. May be given multiple times.
+```
+
+### SEE ALSO
+
+* [ks](ks.md)	 - Configure your application to deploy to a Kubernetes cluster
+
diff --git a/env/create.go b/env/create.go
new file mode 100644
index 0000000000000000000000000000000000000000..8a047fa362c3ba9551788f640507609bcca730cd
--- /dev/null
+++ b/env/create.go
@@ -0,0 +1,124 @@
+package env
+
+import (
+	"fmt"
+	"path"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/pkg/errors"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+)
+
+// CreateConfig is configuration for creating an environment.
+type CreateConfig struct {
+	App         app.App
+	Destination Destination
+	Fs          afero.Fs
+	K8sSpecFlag string
+	Name        string
+	RootPath    string
+
+	OverrideData []byte
+	ParamsData   []byte
+}
+
+// Create creates a new environment for the project.
+func Create(config CreateConfig) error {
+	c, err := newCreator(config)
+	if err != nil {
+		return err
+	}
+	return c.Create()
+}
+
+type creator struct {
+	CreateConfig
+}
+
+func newCreator(config CreateConfig) (*creator, error) {
+	return &creator{
+		CreateConfig: config,
+	}, nil
+}
+
+func (c *creator) Create() error {
+	if c.environmentExists() {
+		return errors.Errorf("Could not create %q", c.Name)
+	}
+
+	// ensure environment name does not contain punctuation
+	if !isValidName(c.Name) {
+		return fmt.Errorf("Environment name %q is not valid; must not contain punctuation, spaces, or begin or end with a slash", c.Name)
+	}
+
+	log.Infof("Creating environment %q with namespace %q, pointing to cluster at address %q",
+		c.Name, c.Destination.Namespace(), c.Destination.Server())
+
+	envPath := filepath.Join(c.RootPath, app.EnvironmentDirName, c.Name)
+	err := c.Fs.MkdirAll(envPath, app.DefaultFolderPermissions)
+	if err != nil {
+		return err
+	}
+
+	metadata := []struct {
+		path string
+		data []byte
+	}{
+		{
+			// environment base override file
+			filepath.Join(envPath, envFileName),
+			c.OverrideData,
+		},
+		{
+			// params file
+			filepath.Join(envPath, paramsFileName),
+			c.ParamsData,
+		},
+	}
+
+	for _, a := range metadata {
+		fileName := path.Base(a.path)
+		log.Debugf("Generating '%s', length: %d", fileName, len(a.data))
+		if err = afero.WriteFile(c.Fs, a.path, a.data, app.DefaultFilePermissions); err != nil {
+			log.Debugf("Failed to write '%s'", fileName)
+			return err
+		}
+	}
+
+	// update app.yaml
+	err = c.App.AddEnvironment(c.Name, c.K8sSpecFlag, &app.EnvironmentSpec{
+		Path: c.Name,
+		Destination: &app.EnvironmentDestinationSpec{
+			Server:    c.Destination.Server(),
+			Namespace: c.Destination.Namespace(),
+		},
+	})
+
+	return err
+}
+
+func (c *creator) environmentExists() bool {
+	_, err := c.App.Environment(c.Name)
+	return err == nil
+}
+
+// isValidName returns true if a name (e.g., for an environment) is valid.
+// Broadly, this means it does not contain punctuation, whitespace, leading or
+// trailing slashes.
+func isValidName(name string) bool {
+	// No unicode whitespace is allowed. `Fields` doesn't handle trailing or
+	// leading whitespace.
+	fields := strings.Fields(name)
+	if len(fields) > 1 || len(strings.TrimSpace(name)) != len(name) {
+		return false
+	}
+
+	hasPunctuation := regexp.MustCompile(`[\\,;':!()?"{}\[\]*&%@$]+`).MatchString
+	hasTrailingSlashes := regexp.MustCompile(`/+$`).MatchString
+	hasLeadingSlashes := regexp.MustCompile(`^/+`).MatchString
+	return len(name) != 0 && !hasPunctuation(name) && !hasTrailingSlashes(name) && !hasLeadingSlashes(name)
+}
diff --git a/env/create_test.go b/env/create_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..398edc8cfdb0a03c8904bf94882775908ceea724
--- /dev/null
+++ b/env/create_test.go
@@ -0,0 +1,40 @@
+package env
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/mock"
+
+	"github.com/ksonnet/ksonnet/metadata/app/mocks"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCreate(t *testing.T) {
+	withEnv(t, func(fs afero.Fs) {
+		appMock := &mocks.App{}
+		appMock.On("Environment", "newenv").Return(nil, errors.New("it does not exist"))
+		appMock.On(
+			"AddEnvironment",
+			"newenv",
+			"version:v1.8.7",
+			mock.AnythingOfType("*app.EnvironmentSpec"),
+		).Return(nil)
+
+		config := CreateConfig{
+			App:         appMock,
+			Fs:          fs,
+			Destination: NewDestination("http://example.com", "default"),
+			RootPath:    "/",
+			Name:        "newenv",
+			K8sSpecFlag: "version:v1.8.7",
+		}
+
+		err := Create(config)
+		require.NoError(t, err)
+
+		checkExists(t, fs, "/environments/newenv/main.jsonnet")
+		checkExists(t, fs, "/environments/newenv/params.libsonnet")
+	})
+}
diff --git a/env/delete.go b/env/delete.go
new file mode 100644
index 0000000000000000000000000000000000000000..4779b6e0f78b12fe0125cb72ca85d3abd1bde18a
--- /dev/null
+++ b/env/delete.go
@@ -0,0 +1,63 @@
+package env
+
+import (
+	"path/filepath"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+)
+
+// DeleteConfig is a configuration for deleting an environment.
+type DeleteConfig struct {
+	App     app.App
+	AppRoot string
+	Name    string
+	Fs      afero.Fs
+}
+
+// Delete deletes an environment.
+func Delete(config DeleteConfig) error {
+	d, err := newDeleter(config)
+	if err != nil {
+		return err
+	}
+	return d.Delete()
+}
+
+type deleter struct {
+	DeleteConfig
+}
+
+func newDeleter(config DeleteConfig) (*deleter, error) {
+	return &deleter{
+		DeleteConfig: config,
+	}, nil
+}
+
+func (d *deleter) Delete() error {
+	envPath, err := filepath.Abs(filepath.Join(d.AppRoot, envRoot, d.Name))
+	if err != nil {
+		return err
+	}
+
+	log.Infof("Deleting environment %q with metadata at path %q", d.Name, envPath)
+
+	// Remove the directory and all files within the environment path.
+	if err = d.Fs.RemoveAll(envPath); err != nil {
+		// if err = d.cleanEmptyParentDirs(); err != nil {
+		log.Debugf("Failed to remove environment directory at path %q", envPath)
+		return err
+	}
+
+	if err = d.App.RemoveEnvironment(d.Name); err != nil {
+		return err
+	}
+
+	if err = cleanEmptyDirs(d.Fs, d.AppRoot); err != nil {
+		return err
+	}
+
+	log.Infof("Successfully removed environment '%s'", d.Name)
+	return nil
+}
diff --git a/env/delete_test.go b/env/delete_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..403cc50756d4235006ffa1944bd5d1572e053feb
--- /dev/null
+++ b/env/delete_test.go
@@ -0,0 +1,28 @@
+package env
+
+import (
+	"testing"
+
+	"github.com/ksonnet/ksonnet/metadata/app/mocks"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDelete(t *testing.T) {
+	withEnv(t, func(fs afero.Fs) {
+		appMock := &mocks.App{}
+		appMock.On("RemoveEnvironment", "nested/env3").Return(nil)
+
+		config := DeleteConfig{
+			App:     appMock,
+			Fs:      fs,
+			Name:    "nested/env3",
+			AppRoot: "/",
+		}
+
+		err := Delete(config)
+		require.NoError(t, err)
+
+		checkNotExists(t, fs, "/environments/nested")
+	})
+}
diff --git a/env/destination.go b/env/destination.go
new file mode 100644
index 0000000000000000000000000000000000000000..ba2312e1453aed4e0c74f2c63231fad4f4e197ea
--- /dev/null
+++ b/env/destination.go
@@ -0,0 +1,48 @@
+package env
+
+import "encoding/json"
+
+const (
+	// destDefaultNamespace is the default namespace name.
+	destDefaultNamespace = "default"
+)
+
+// Destination contains destination information for a cluster.
+type Destination struct {
+	server    string
+	namespace string
+}
+
+// NewDestination creates an instance of Destination.
+func NewDestination(server, namespace string) Destination {
+	return Destination{
+		server:    server,
+		namespace: namespace,
+	}
+}
+
+// MarshalJSON marshals a Destination to JSON.
+func (d *Destination) MarshalJSON() ([]byte, error) {
+	return json.Marshal(&struct {
+		Server    string `json:"server"`
+		Namespace string `json:"namespace"`
+	}{
+		Server:    d.Server(),
+		Namespace: d.Namespace(),
+	})
+}
+
+// Server is URL to the Kubernetes server that the cluster is running on.
+func (d *Destination) Server() string {
+	return d.server
+}
+
+// Namespace is the namespace of the Kubernetes server that targets should
+// be deployed.
+func (d *Destination) Namespace() string {
+	if d.namespace == "" {
+		return destDefaultNamespace
+	}
+
+	return d.namespace
+}
diff --git a/env/env.go b/env/env.go
new file mode 100644
index 0000000000000000000000000000000000000000..ba1beec525be2cd91c2c28da0bc554f766bc28d8
--- /dev/null
+++ b/env/env.go
@@ -0,0 +1,62 @@
+package env
+
+import (
+	"github.com/ksonnet/ksonnet/metadata/app"
+)
+
+const (
+	// primary environment files.
+	envFileName    = "main.jsonnet"
+	paramsFileName = "params.libsonnet"
+
+	// envRoot is the name for the environment root.
+	envRoot = "environments"
+)
+
+// Env represents a ksonnet environment.
+type Env struct {
+	// Name is the environment name.
+	Name string
+	// KubernetesVersion is the version of Kubernetes for this environment.
+	KubernetesVersion string
+	// Destination is the cluster destination for this environment.
+	Destination Destination
+	// Targets are the component namespaces that will be installed.
+	Targets []string
+}
+
+func envFromSpec(name string, envSpec *app.EnvironmentSpec) *Env {
+	return &Env{
+		Name:              name,
+		KubernetesVersion: envSpec.KubernetesVersion,
+		Destination:       NewDestination(envSpec.Destination.Server, envSpec.Destination.Namespace),
+		Targets:           envSpec.Targets,
+	}
+}
+
+// List lists all environments for the current ksonnet application.
+func List(ksApp app.App) (map[string]Env, error) {
+	envs := make(map[string]Env)
+
+	specs, err := ksApp.Environments()
+	if err != nil {
+		return nil, err
+	}
+
+	for name, spec := range specs {
+		env := envFromSpec(name, spec)
+		envs[name] = *env
+	}
+
+	return envs, nil
+}
+
+// Retrieve retrieves an environment by name.
+func Retrieve(ksApp app.App, name string) (*Env, error) {
+	envSpec, err := ksApp.Environment(name)
+	if err != nil {
+		return nil, err
+	}
+
+	return envFromSpec(name, envSpec), nil
+}
diff --git a/env/env_test.go b/env/env_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..6a6e74f63bf745d459350253e8eea226729d7df0
--- /dev/null
+++ b/env/env_test.go
@@ -0,0 +1,41 @@
+package env
+
+import (
+	"testing"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/ksonnet/ksonnet/metadata/app/mocks"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func TestList(t *testing.T) {
+	withEnv(t, func(fs afero.Fs) {
+		appMock := &mocks.App{}
+
+		specEnvs := app.EnvironmentSpecs{
+			"default": &app.EnvironmentSpec{
+				Path: "default",
+				Destination: &app.EnvironmentDestinationSpec{
+					Namespace: "default",
+					Server:    "http://example.com",
+				},
+				KubernetesVersion: "v1.8.7",
+			},
+		}
+		appMock.On("Environments").Return(specEnvs, nil)
+
+		envs, err := List(appMock)
+		require.NoError(t, err)
+
+		expected := map[string]Env{
+			"default": Env{
+				KubernetesVersion: "v1.8.7",
+				Name:              "default",
+				Destination:       NewDestination("http://example.com", "default"),
+			},
+		}
+
+		require.Equal(t, expected, envs)
+	})
+}
diff --git a/env/params.go b/env/params.go
new file mode 100644
index 0000000000000000000000000000000000000000..b17add27c3e646cc68d47fc566ac5da039f524c7
--- /dev/null
+++ b/env/params.go
@@ -0,0 +1,103 @@
+package env
+
+import (
+	"github.com/ksonnet/ksonnet/component"
+	"github.com/ksonnet/ksonnet/metadata/app"
+	param "github.com/ksonnet/ksonnet/metadata/params"
+	"github.com/pkg/errors"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+)
+
+// SetParamsConfig is config items for setting environment params.
+type SetParamsConfig struct {
+	AppRoot string
+	Fs      afero.Fs
+}
+
+// SetParams sets params for an environment.
+func SetParams(envName, component string, params param.Params, config SetParamsConfig) error {
+	exists, err := envExists(config.Fs, config.AppRoot, envName)
+	if err != nil {
+		return err
+	}
+	if !exists {
+		return errors.Errorf("Environment %q does not exist", envName)
+	}
+
+	path := envPath(config.AppRoot, envName, paramsFileName)
+
+	text, err := afero.ReadFile(config.Fs, path)
+	if err != nil {
+		return err
+	}
+
+	appended, err := param.SetEnvironmentParams(component, string(text), params)
+	if err != nil {
+		return err
+	}
+
+	err = afero.WriteFile(config.Fs, path, []byte(appended), app.DefaultFilePermissions)
+	if err != nil {
+		return err
+	}
+
+	log.Debugf("Successfully set parameters for component %q at environment %q", component, envName)
+	return nil
+}
+
+// GetParamsConfig is config items for getting environment params.
+type GetParamsConfig struct {
+	AppRoot string
+	Fs      afero.Fs
+}
+
+// GetParams gets all parameters for an environment.
+func GetParams(envName, nsName string, config GetParamsConfig) (map[string]param.Params, error) {
+	exists, err := envExists(config.Fs, config.AppRoot, envName)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.Errorf("Environment %q does not exist", envName)
+	}
+
+	// Get the environment specific params
+	envParamsPath := envPath(config.AppRoot, envName, paramsFileName)
+	envParamsText, err := afero.ReadFile(config.Fs, envParamsPath)
+	if err != nil {
+		return nil, err
+	}
+	envParams, err := param.GetAllEnvironmentParams(string(envParamsText))
+	if err != nil {
+		return nil, err
+	}
+
+	// figure out what component we need
+	ns := component.NewNamespace(config.Fs, config.AppRoot, nsName)
+	componentParamsFile, err := afero.ReadFile(config.Fs, ns.ParamsPath())
+	if err != nil {
+		return nil, err
+	}
+
+	componentParams, err := param.GetAllComponentParams(string(componentParamsFile))
+	if err != nil {
+		return nil, err
+	}
+
+	return mergeParamMaps(componentParams, envParams), nil
+}
+
+// TODO: move this to the consolidated params support namespace.
+func mergeParamMaps(base, overrides map[string]param.Params) map[string]param.Params {
+	for component, params := range overrides {
+		if _, contains := base[component]; !contains {
+			base[component] = params
+		} else {
+			for k, v := range params {
+				base[component][k] = v
+			}
+		}
+	}
+	return base
+}
diff --git a/env/params_test.go b/env/params_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8b09b39cf9afbf327c86b21b37b90f7a92e177e4
--- /dev/null
+++ b/env/params_test.go
@@ -0,0 +1,103 @@
+package env
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/ksonnet/ksonnet/metadata/params"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSetParams(t *testing.T) {
+	withEnv(t, func(fs afero.Fs) {
+		config := SetParamsConfig{
+			AppRoot: "/",
+			Fs:      fs,
+		}
+
+		p := params.Params{
+			"foo": "bar",
+		}
+
+		err := SetParams("env1", "component1", p, config)
+		require.NoError(t, err)
+
+		compareOutput(t, fs, "updated-params.libsonnet", "/environments/env1/params.libsonnet")
+	})
+}
+
+func TestGetParams(t *testing.T) {
+	withEnv(t, func(fs afero.Fs) {
+		config := GetParamsConfig{
+			AppRoot: "/",
+			Fs:      fs,
+		}
+
+		p, err := GetParams("env1", "", config)
+		require.NoError(t, err)
+
+		expected := map[string]params.Params{
+			"component1": params.Params{
+				"foo": `"bar"`,
+			},
+		}
+
+		require.Equal(t, expected, p)
+	})
+}
+
+func TestMergeParamMaps(t *testing.T) {
+	tests := []struct {
+		base      map[string]params.Params
+		overrides map[string]params.Params
+		expected  map[string]params.Params
+	}{
+		{
+			map[string]params.Params{
+				"bar": params.Params{"replicas": "5"},
+			},
+			map[string]params.Params{
+				"foo": params.Params{"name": `"foo"`, "replicas": "1"},
+			},
+			map[string]params.Params{
+				"bar": params.Params{"replicas": "5"},
+				"foo": params.Params{"name": `"foo"`, "replicas": "1"},
+			},
+		},
+		{
+			map[string]params.Params{
+				"bar": params.Params{"replicas": "5"},
+			},
+			map[string]params.Params{
+				"bar": params.Params{"name": `"foo"`},
+			},
+			map[string]params.Params{
+				"bar": params.Params{"name": `"foo"`, "replicas": "5"},
+			},
+		},
+		{
+			map[string]params.Params{
+				"bar": params.Params{"name": `"bar"`, "replicas": "5"},
+				"foo": params.Params{"name": `"foo"`, "replicas": "4"},
+				"baz": params.Params{"name": `"baz"`, "replicas": "3"},
+			},
+			map[string]params.Params{
+				"foo": params.Params{"replicas": "1"},
+				"baz": params.Params{"name": `"foobaz"`},
+			},
+			map[string]params.Params{
+				"bar": params.Params{"name": `"bar"`, "replicas": "5"},
+				"foo": params.Params{"name": `"foo"`, "replicas": "1"},
+				"baz": params.Params{"name": `"foobaz"`, "replicas": "3"},
+			},
+		},
+	}
+
+	for _, s := range tests {
+		result := mergeParamMaps(s.base, s.overrides)
+		if !reflect.DeepEqual(s.expected, result) {
+			t.Errorf("Wrong merge\n  expected:\n%v\n  got:\n%v", s.expected, result)
+		}
+	}
+}
diff --git a/env/rename.go b/env/rename.go
new file mode 100644
index 0000000000000000000000000000000000000000..51880a5154357e4a9eeef985aee822a36710194d
--- /dev/null
+++ b/env/rename.go
@@ -0,0 +1,179 @@
+package env
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/pkg/errors"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+)
+
+// RenameConfig are options for renaming an environment.
+type RenameConfig struct {
+	App     app.App
+	AppRoot string
+	Fs      afero.Fs
+}
+
+// Rename renames an environment
+func Rename(from, to string, config RenameConfig) error {
+	r, err := newRenamer(config)
+	if err != nil {
+		return err
+	}
+	return r.Rename(from, to)
+}
+
+type renamer struct {
+	RenameConfig
+}
+
+func newRenamer(config RenameConfig) (*renamer, error) {
+	return &renamer{
+		RenameConfig: config,
+	}, nil
+}
+
+func (r *renamer) Rename(from, to string) error {
+	if from == to || to == "" {
+		return nil
+	}
+
+	if err := r.preflight(from, to); err != nil {
+		return err
+	}
+
+	pathFrom := envPath(r.AppRoot, from)
+	pathTo := envPath(r.AppRoot, to)
+
+	exists, err := afero.DirExists(r.Fs, pathFrom)
+	if err != nil {
+		return err
+	}
+
+	if !exists {
+		return errors.Errorf("environment directory for %q does not exist", from)
+	}
+
+	log.Infof("Setting environment name from %q to %q", from, to)
+
+	current, err := Retrieve(r.App, from)
+	if err != nil {
+		return err
+	}
+
+	update := &app.EnvironmentSpec{
+		Destination: &app.EnvironmentDestinationSpec{
+			Namespace: current.Destination.Namespace(),
+			Server:    current.Destination.Server(),
+		},
+		KubernetesVersion: current.KubernetesVersion,
+		Targets:           current.Targets,
+		Path:              to,
+	}
+
+	k8sSpecFlag := fmt.Sprintf("version:%s", current.KubernetesVersion)
+
+	if err = r.App.AddEnvironment(from, k8sSpecFlag, update); err != nil {
+		return err
+	}
+
+	if err = moveDir(r.Fs, pathFrom, pathTo); err != nil {
+		return err
+	}
+
+	if err = cleanEmptyDirs(r.Fs, r.AppRoot); err != nil {
+		return errors.Wrap(err, "clean empty directories")
+	}
+
+	log.Infof("Successfully moved %q to %q", from, to)
+	return nil
+}
+
+func (r *renamer) preflight(from, to string) error {
+	if !isValidName(to) {
+		return fmt.Errorf("Environment name %q is not valid; must not contain punctuation, spaces, or begin or end with a slash",
+			to)
+	}
+
+	exists, err := envExists(r.Fs, r.AppRoot, to)
+	if err != nil {
+		log.Debugf("Failed to check whether environment %q already exists", to)
+		return err
+	}
+	if exists {
+		return fmt.Errorf("Failed to update %q; environment %q exists", from, to)
+	}
+
+	return nil
+}
+
+func envExists(fs afero.Fs, appRoot, name string) (bool, error) {
+	path := envPath(appRoot, name, envFileName)
+	return afero.Exists(fs, path)
+}
+
+func moveDir(fs afero.Fs, src, dest string) error {
+	exists, err := afero.DirExists(fs, dest)
+	if err != nil {
+		return err
+	}
+
+	if !exists {
+		if err = fs.MkdirAll(dest, app.DefaultFolderPermissions); err != nil {
+			return errors.Wrapf(err, "unable to create destination %q", dest)
+		}
+	}
+
+	fis, err := afero.ReadDir(fs, src)
+	if err != nil {
+		return err
+	}
+
+	for _, fi := range fis {
+		if fi.IsDir() && fi.Name() != ".metadata" {
+			continue
+		}
+
+		srcPath := filepath.Join(src, fi.Name())
+		destPath := filepath.Join(dest, fi.Name())
+
+		if err = fs.Rename(srcPath, destPath); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func envPath(root, name string, subPath ...string) string {
+	return filepath.Join(append([]string{root, envRoot, name}, subPath...)...)
+}
+
+func cleanEmptyDirs(fs afero.Fs, root string) error {
+	log.Debug("Removing empty environment directories, if any")
+	envPath := filepath.Join(root, envRoot)
+	return afero.Walk(fs, envPath, func(path string, fi os.FileInfo, err error) error {
+		if err != nil {
+			return nil
+		}
+
+		if !fi.IsDir() {
+			return nil
+		}
+
+		isEmpty, err := afero.IsEmpty(fs, path)
+		if err != nil {
+			log.Debugf("Failed to check whether directory at path %q is empty", path)
+			return err
+		}
+		if isEmpty {
+			return fs.RemoveAll(path)
+		}
+		return nil
+	})
+}
diff --git a/env/rename_test.go b/env/rename_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a490275d4ca18466ac122042baed635e7facf975
--- /dev/null
+++ b/env/rename_test.go
@@ -0,0 +1,46 @@
+package env
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/mock"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/ksonnet/ksonnet/metadata/app/mocks"
+)
+
+func TestRename(t *testing.T) {
+	withEnv(t, func(fs afero.Fs) {
+		appMock := &mocks.App{}
+
+		envSpec := &app.EnvironmentSpec{
+			Path:              "env1",
+			Destination:       &app.EnvironmentDestinationSpec{Namespace: "default", Server: "http://example.com"},
+			KubernetesVersion: "v1.9.2",
+		}
+		appMock.On("Environment", "env1").Return(envSpec, nil)
+
+		appMock.On(
+			"AddEnvironment",
+			"env1",
+			"version:v1.9.2",
+			mock.AnythingOfType("*app.EnvironmentSpec")).Return(nil)
+
+		config := RenameConfig{
+			App:     appMock,
+			AppRoot: "/",
+			Fs:      fs,
+		}
+
+		checkExists(t, fs, "/environments/env1")
+
+		err := Rename("env1", "env1-updated", config)
+		require.NoError(t, err)
+
+		checkNotExists(t, fs, "/environments/env1")
+		checkExists(t, fs, "/environments/env1-updated/main.jsonnet")
+	})
+}
diff --git a/env/testdata/app.yaml b/env/testdata/app.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a7e78db29e8079580fda8885be220a55731c536c
--- /dev/null
+++ b/env/testdata/app.yaml
@@ -0,0 +1,28 @@
+apiVersion: 0.1.0
+environments:
+  env1:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: v1.7.0
+    path: env1
+  env2:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: ""
+    path: env2
+  nested/env3:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: ""
+    path: nest/env3
+kind: ksonnet.io/app
+name: test-get-envs
+registries:
+  incubator:
+    gitVersion: null
+    protocol: ""
+    uri: ""
+version: 0.0.1
\ No newline at end of file
diff --git a/env/testdata/component-params.libsonnet b/env/testdata/component-params.libsonnet
new file mode 100644
index 0000000000000000000000000000000000000000..493125a07f2fc262e69ed14867071c672866ba0e
--- /dev/null
+++ b/env/testdata/component-params.libsonnet
@@ -0,0 +1,10 @@
+{
+  global: {
+    // User-defined global parameters; accessible to all component and environments, Ex:
+    // replicas: 4,
+  },
+  components: {
+    // Component-level parameters, defined initially from 'ks prototype use ...'
+    // Each object below should correspond to a component in the components/ directory
+  },
+}
\ No newline at end of file
diff --git a/env/testdata/main.jsonnet b/env/testdata/main.jsonnet
new file mode 100644
index 0000000000000000000000000000000000000000..2dae67b25f2b127f7ac87d5bfa24aaab7eacb880
--- /dev/null
+++ b/env/testdata/main.jsonnet
@@ -0,0 +1 @@
+// main.jsonnet
\ No newline at end of file
diff --git a/env/testdata/params.libsonnet b/env/testdata/params.libsonnet
new file mode 100644
index 0000000000000000000000000000000000000000..be827610cbd6e29efdb1de4f5972f72ab52a9a65
--- /dev/null
+++ b/env/testdata/params.libsonnet
@@ -0,0 +1,13 @@
+local params = import "../../components/params.libsonnet";
+params + {
+  components +: {
+    // Insert component parameter overrides here. Ex:
+    // guestbook +: {
+    //   name: "guestbook-dev",
+    //   replicas: params.global.replicas,
+    // },
+    component1 +: {
+      foo: "bar",
+    },
+  },
+}
diff --git a/env/testdata/updated-params.libsonnet b/env/testdata/updated-params.libsonnet
new file mode 100644
index 0000000000000000000000000000000000000000..563086b30b3e9231288b9ddf9f9f215148207019
--- /dev/null
+++ b/env/testdata/updated-params.libsonnet
@@ -0,0 +1,13 @@
+local params = import "../../components/params.libsonnet";
+params + {
+  components +: {
+    // Insert component parameter overrides here. Ex:
+    // guestbook +: {
+    //   name: "guestbook-dev",
+    //   replicas: params.global.replicas,
+    // },
+    component1 +: {
+      foo: bar,
+    },
+  },
+}
diff --git a/env/util_test.go b/env/util_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..fb2420943a729649b18bb01bacd0733494354653
--- /dev/null
+++ b/env/util_test.go
@@ -0,0 +1,83 @@
+package env
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func stageFile(t *testing.T, fs afero.Fs, src, dest string) {
+	in := filepath.Join("testdata", src)
+
+	b, err := ioutil.ReadFile(in)
+	require.NoError(t, err)
+
+	dir := filepath.Dir(dest)
+	err = fs.MkdirAll(dir, 0755)
+	require.NoError(t, err)
+
+	err = afero.WriteFile(fs, dest, b, 0644)
+	require.NoError(t, err)
+}
+
+func withEnv(t *testing.T, fn func(afero.Fs)) {
+	tmpDir, err := ioutil.TempDir("", "")
+	require.NoError(t, err)
+	defer os.RemoveAll(tmpDir)
+
+	// NOTE: using an os fs here because afero doesn't handle renames in the memmap version
+	fs := afero.NewBasePathFs(afero.NewOsFs(), tmpDir)
+	stageFile(t, fs, "app.yaml", "/app.yaml")
+
+	dirs := []string{
+		"env1",
+		"env2",
+		"nest/env3",
+	}
+
+	for _, dir := range dirs {
+		path := filepath.Join("/", envRoot, dir)
+		err := fs.MkdirAll(path, app.DefaultFolderPermissions)
+		require.NoError(t, err)
+
+		mainPath := filepath.Join(path, "main.jsonnet")
+		stageFile(t, fs, "main.jsonnet", mainPath)
+
+		paramsPath := filepath.Join(path, "params.libsonnet")
+		stageFile(t, fs, "params.libsonnet", paramsPath)
+	}
+
+	componentParamsPath := filepath.Join("/", "components", "params.libsonnet")
+	stageFile(t, fs, "component-params.libsonnet", componentParamsPath)
+
+	fn(fs)
+}
+
+func checkExists(t *testing.T, fs afero.Fs, path string) {
+	exists, err := afero.Exists(fs, path)
+	require.NoError(t, err)
+
+	require.True(t, exists, "%q should exist", path)
+}
+
+func checkNotExists(t *testing.T, fs afero.Fs, path string) {
+	exists, err := afero.Exists(fs, path)
+	require.NoError(t, err)
+
+	require.False(t, exists, "%q should not exist", path)
+}
+
+func compareOutput(t *testing.T, fs afero.Fs, expected, got string) {
+	gotData, err := afero.ReadFile(fs, got)
+	require.NoError(t, err)
+
+	expectedData, err := ioutil.ReadFile(filepath.Join("testdata", expected))
+	require.NoError(t, err)
+
+	require.Equal(t, string(expectedData), string(gotData))
+}
diff --git a/generator/ksonnet.go b/generator/ksonnet.go
index 647fa7811d8b9bb65796fa08bd4178a43b87aaf1..2933d3768221a87f34ccdb90d9b815a46cfe9315 100644
--- a/generator/ksonnet.go
+++ b/generator/ksonnet.go
@@ -1,16 +1,22 @@
 package generator
 
 import (
+	"encoding/json"
 	"io/ioutil"
 	"os"
+	"strings"
+
+	"github.com/blang/semver"
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
 
-	"github.com/davecgh/go-spew/spew"
 	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet"
 )
 
 var (
 	// ksonnetEmitter is the function which emits the ksonnet standard library.
 	ksonnetEmitter = ksonnet.GenerateLib
+	// revision is the current revision of ksonnet based on the git ref.
+	revision string
 )
 
 // KsonnetLib is the ksonnet standard library for a version of swagger.
@@ -35,6 +41,16 @@ func Ksonnet(swaggerData []byte) (*KsonnetLib, error) {
 
 	defer os.Remove(f.Name())
 
+	var apiSpec kubespec.APISpec
+	if err = json.Unmarshal(swaggerData, &apiSpec); err != nil {
+		return nil, err
+	}
+
+	ver, err := semver.Make(strings.TrimPrefix(apiSpec.Info.Version, "v"))
+	if err != nil {
+		return nil, err
+	}
+
 	_, err = f.Write(swaggerData)
 	if err != nil {
 		return nil, err
@@ -44,9 +60,16 @@ func Ksonnet(swaggerData []byte) (*KsonnetLib, error) {
 		return nil, err
 	}
 
-	spew.Dump("---", f.Name(), ksonnetEmitter)
+	astMin := semver.MustParse("1.8.0")
+	if ver.LT(astMin) {
+		return textBuilder(&apiSpec, swaggerData)
+	}
+
+	return astBuilder(f.Name(), swaggerData)
+}
 
-	lib, err := ksonnetEmitter(f.Name())
+func astBuilder(input string, swaggerData []byte) (*KsonnetLib, error) {
+	lib, err := ksonnetEmitter(input)
 	if err != nil {
 		return nil, err
 	}
@@ -60,3 +83,19 @@ func Ksonnet(swaggerData []byte) (*KsonnetLib, error) {
 
 	return kl, nil
 }
+
+func textBuilder(apiSpec *kubespec.APISpec, swaggerData []byte) (*KsonnetLib, error) {
+	bK, bK8s, err := ksonnet.Emit(apiSpec, &revision, &revision)
+	if err != nil {
+		return nil, err
+	}
+
+	kl := &KsonnetLib{
+		K:       bK,
+		K8s:     bK8s,
+		Swagger: swaggerData,
+		Version: apiSpec.Info.Version,
+	}
+
+	return kl, nil
+}
diff --git a/generator/ksonnet_test.go b/generator/ksonnet_test.go
index 91b0dd80f3904f93a94a8b917f429a71fef101fe..c2cb990dc75a6a936b5bb99ddb6e0393c806f050 100644
--- a/generator/ksonnet_test.go
+++ b/generator/ksonnet_test.go
@@ -18,7 +18,7 @@ func TestKsonnet(t *testing.T) {
 		lib            = []byte("k8s")
 		successfulEmit = func(string) (*ksonnet.Lib, error) {
 			return &ksonnet.Lib{
-				Version:    "v1.7.0",
+				Version:    "v1.8.0",
 				K8s:        lib,
 				Extensions: ext,
 			}, nil
@@ -26,7 +26,7 @@ func TestKsonnet(t *testing.T) {
 		failureEmit = func(string) (*ksonnet.Lib, error) {
 			return nil, errors.New("failure")
 		}
-		v170swagger = []byte(`{"info":{"version":"v1.7.0"}}`)
+		v180swagger = []byte(`{"info":{"version":"v1.8.0"}}`)
 	)
 
 	cases := []struct {
@@ -39,8 +39,8 @@ func TestKsonnet(t *testing.T) {
 		{
 			name:        "valid swagger",
 			emitter:     successfulEmit,
-			swaggerData: v170swagger,
-			version:     "v1.7.0",
+			swaggerData: v180swagger,
+			version:     "v1.8.0",
 		},
 		{
 			name:        "invalid swagger",
@@ -51,7 +51,7 @@ func TestKsonnet(t *testing.T) {
 		{
 			name:        "emitter error",
 			emitter:     failureEmit,
-			swaggerData: v170swagger,
+			swaggerData: v180swagger,
 			isErr:       true,
 		},
 	}
diff --git a/integration/integration_suite_test.go b/integration/integration_suite_test.go
index f44a534f06580cffd757e373d8224ac1f3931cc8..363b342fb0846c94de74a953f5d9eca5becefb17 100644
--- a/integration/integration_suite_test.go
+++ b/integration/integration_suite_test.go
@@ -3,7 +3,6 @@
 package integration
 
 import (
-	"github.com/ksonnet/ksonnet/metadata/app"
 	"flag"
 	"fmt"
 	"io"
@@ -12,11 +11,13 @@ import (
 	"path"
 	"testing"
 
+	"github.com/ksonnet/ksonnet/metadata/app"
+
+	"k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/apimachinery/registered"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
-	"k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/apimachinery/registered"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
 
@@ -90,8 +91,8 @@ func containsString(haystack []string, needle string) bool {
 
 func runKsonnetWith(flags []string, host, ns string) error {
 	spec := app.Spec{
-		Version: "0.0.1",
-		APIVersion: "0.0.1",
+		Version:    "0.0.1",
+		APIVersion: "0.1.0",
 		Environments: app.EnvironmentSpecs{
 			"default": &app.EnvironmentSpec{
 				Destination: &app.EnvironmentDestinationSpec{
diff --git a/metadata/app.go b/metadata/app.go
deleted file mode 100644
index f8614de0b2b867f833c40916defef44488b785c7..0000000000000000000000000000000000000000
--- a/metadata/app.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package metadata
-
-import (
-	"github.com/ksonnet/ksonnet/metadata/app"
-	"github.com/spf13/afero"
-)
-
-// AppSpec will return the specification for a ksonnet application
-// (typically stored in `app.yaml`)
-func (m *manager) AppSpec() (*app.Spec, error) {
-	bytes, err := afero.ReadFile(m.appFS, string(m.appYAMLPath))
-	if err != nil {
-		return nil, err
-	}
-
-	schema, err := app.Unmarshal(bytes)
-	if err != nil {
-		return nil, err
-	}
-
-	if schema.Contributors == nil {
-		schema.Contributors = app.ContributorSpecs{}
-	}
-
-	if schema.Registries == nil {
-		schema.Registries = app.RegistryRefSpecs{}
-	}
-
-	if schema.Libraries == nil {
-		schema.Libraries = app.LibraryRefSpecs{}
-	}
-
-	if schema.Environments == nil {
-		schema.Environments = app.EnvironmentSpecs{}
-	}
-
-	return schema, nil
-}
-
-// WriteAppSpec writes the provided spec to the app.yaml file.
-func (m *manager) WriteAppSpec(appSpec *app.Spec) error {
-	appSpecData, err := appSpec.Marshal()
-	if err != nil {
-		return err
-	}
-
-	return afero.WriteFile(m.appFS, string(m.appYAMLPath), appSpecData, defaultFilePermissions)
-}
diff --git a/metadata/app/app.go b/metadata/app/app.go
new file mode 100644
index 0000000000000000000000000000000000000000..7e964470f17a37e5646f85ea90e2d25c07e7ae7d
--- /dev/null
+++ b/metadata/app/app.go
@@ -0,0 +1,80 @@
+package app
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/ksonnet/ksonnet/metadata/lib"
+	"github.com/pkg/errors"
+	"github.com/spf13/afero"
+)
+
+const (
+
+	// appYamlName is the name for the app specification.
+	appYamlName = "app.yaml"
+
+	// EnvironmentDirName is the directory name for environments.
+	EnvironmentDirName = "environments"
+
+	// LibDirName is the directory name for libraries.
+	LibDirName = "lib"
+)
+
+var (
+	// DefaultFilePermissions are the default permissions for a file.
+	DefaultFilePermissions = os.FileMode(0644)
+	// DefaultFolderPermissions are the default permissions for a folder.
+	DefaultFolderPermissions = os.FileMode(0755)
+
+	// LibUpdater updates ksonnet lib versions.
+	LibUpdater = updateLibData
+)
+
+// App is a ksonnet application.
+type App interface {
+	AddEnvironment(name, k8sSpecFlag string, spec *EnvironmentSpec) error
+	Environment(name string) (*EnvironmentSpec, error)
+	Environments() (EnvironmentSpecs, error)
+	Libraries() LibraryRefSpecs
+	LibPath(envName string) (string, error)
+	Init() error
+	Registries() RegistryRefSpecs
+	RemoveEnvironment(name string) error
+	Upgrade(dryRun bool) error
+}
+
+// Load loads the application configuration.
+func Load(fs afero.Fs, appRoot string) (App, error) {
+	spec, err := Read(fs, appRoot)
+	if err != nil {
+		return nil, err
+	}
+
+	switch spec.APIVersion {
+	default:
+		return nil, errors.Errorf("unknown apiVersion %q in %s", spec.APIVersion, appYamlName)
+	case "0.0.1":
+		return NewApp001(fs, appRoot)
+	case "0.1.0":
+		return NewApp010(fs, appRoot)
+	}
+}
+
+func updateLibData(fs afero.Fs, k8sSpecFlag string, libPath string, useVersionPath bool) error {
+	lm, err := lib.NewManager(k8sSpecFlag, fs, libPath)
+	if err != nil {
+		return err
+	}
+
+	return lm.GenerateLibData(useVersionPath)
+}
+
+func app010LibPath(root string) string {
+	return filepath.Join(root, LibDirName)
+}
+
+// StubUpdateLibData always returns no error.
+func StubUpdateLibData(fs afero.Fs, k8sSpecFlag string, libPath string, useVersionPath bool) error {
+	return nil
+}
diff --git a/metadata/app/app001.go b/metadata/app/app001.go
new file mode 100644
index 0000000000000000000000000000000000000000..528f8ba79765ca563d9f60446d61aa18be800b07
--- /dev/null
+++ b/metadata/app/app001.go
@@ -0,0 +1,272 @@
+package app
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/ksonnet/ksonnet/metadata/lib"
+	"github.com/pkg/errors"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+)
+
+const (
+	// app001specJSON is the name for environment schema
+	app001specJSON = "spec.json"
+)
+
+// App001 is a ksonnet 0.0.1 application.
+type App001 struct {
+	spec *Spec
+	root string
+	fs   afero.Fs
+	out  io.Writer
+}
+
+var _ App = (*App001)(nil)
+
+// NewApp001 creates an App001 instance.
+func NewApp001(fs afero.Fs, root string) (*App001, error) {
+	spec, err := Read(fs, root)
+	if err != nil {
+		return nil, err
+	}
+
+	return &App001{
+		spec: spec,
+		fs:   fs,
+		root: root,
+		out:  os.Stdout,
+	}, nil
+}
+
+// Init initializes the App.
+func (a *App001) Init() error {
+	msg := "Your application's apiVersion is below 0.1.0. In order to use all ks features, you " +
+		"can upgrade your application using `ks upgrade`."
+	log.Warn(msg)
+
+	return nil
+}
+
+// AddEnvironment adds an environment spec to the app spec. If the spec already exists,
+// it is overwritten.
+func (a *App001) AddEnvironment(name, k8sSpecFlag string, spec *EnvironmentSpec) error {
+	envPath := filepath.Join(a.root, EnvironmentDirName, name)
+	if err := a.fs.MkdirAll(envPath, DefaultFolderPermissions); err != nil {
+		return err
+	}
+
+	specPath := filepath.Join(envPath, app001specJSON)
+
+	b, err := json.Marshal(spec.Destination)
+	if err != nil {
+		return err
+	}
+
+	if err = afero.WriteFile(a.fs, specPath, b, DefaultFilePermissions); err != nil {
+		return err
+	}
+
+	return LibUpdater(a.fs, k8sSpecFlag, a.appLibPath(name), false)
+}
+
+// Registries returns application registries.
+func (a *App001) Registries() RegistryRefSpecs {
+	return a.spec.Registries
+}
+
+// Libraries returns application libraries.
+func (a *App001) Libraries() LibraryRefSpecs {
+	return a.spec.Libraries
+}
+
+// Environment returns the spec for an environment. In 0.1.0, the file lives in
+// /environments/name/spec.json.
+func (a *App001) Environment(name string) (*EnvironmentSpec, error) {
+	path := filepath.Join(a.root, EnvironmentDirName, name, app001specJSON)
+	return read001EnvSpec(a.fs, name, path)
+}
+
+// Environments returns specs for all environments. In 0.1.0, the environment spec
+// lives in spec.json files.
+func (a *App001) Environments() (EnvironmentSpecs, error) {
+	specs := EnvironmentSpecs{}
+
+	root := filepath.Join(a.root, EnvironmentDirName)
+
+	err := afero.Walk(a.fs, root, func(path string, fi os.FileInfo, err error) error {
+		if fi.IsDir() {
+			return nil
+		}
+
+		if fi.Name() == app001specJSON {
+			dir := filepath.Dir(path)
+			envName := strings.TrimPrefix(dir, root+"/")
+			spec, err := read001EnvSpec(a.fs, envName, path)
+			if err != nil {
+				return err
+			}
+
+			specs[envName] = spec
+		}
+
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return specs, nil
+}
+
+type k8sSchema struct {
+	Info struct {
+		Version string `json:"version,omitempty"`
+	} `json:"info,omitempty"`
+}
+
+func read001EnvSpec(fs afero.Fs, name, path string) (*EnvironmentSpec, error) {
+	b, err := afero.ReadFile(fs, path)
+	if err != nil {
+		return nil, err
+	}
+
+	var s EnvironmentDestinationSpec
+	if err = json.Unmarshal(b, &s); err != nil {
+		return nil, err
+	}
+
+	if s.Namespace == "" {
+		s.Namespace = "default"
+	}
+
+	envPath := filepath.Dir(path)
+	swaggerPath := filepath.Join(envPath, ".metadata", "swagger.json")
+
+	b, err = afero.ReadFile(fs, swaggerPath)
+	if err != nil {
+		return nil, err
+	}
+
+	var ks k8sSchema
+	if err = json.Unmarshal(b, &ks); err != nil {
+		return nil, err
+	}
+
+	if ks.Info.Version == "" {
+		return nil, errors.New("unable to determine environment Kubernetes version")
+	}
+
+	spec := EnvironmentSpec{
+		Path:              name,
+		Destination:       &s,
+		KubernetesVersion: ks.Info.Version,
+	}
+
+	return &spec, nil
+}
+
+// RemoveEnvironment removes an environment.
+func (a *App001) RemoveEnvironment(envName string) error {
+	return nil
+}
+
+// Upgrade upgrades the app to the latest apiVersion.
+func (a *App001) Upgrade(dryRun bool) error {
+	if err := a.load(); err != nil {
+		return err
+	}
+
+	if dryRun {
+		fmt.Fprintf(a.out, "\n[dry run] Upgrading application settings from version 0.0.1 to to 0.1.0.\n")
+	}
+
+	envs, err := a.Environments()
+	if err != nil {
+		return err
+	}
+
+	if dryRun {
+		fmt.Fprintf(a.out, "[dry run] Converting 0.0.1 environments to 0.1.0a:\n")
+	}
+	for _, env := range envs {
+		a.convertEnvironment(env.Path, dryRun)
+	}
+
+	a.spec.APIVersion = "0.1.0"
+
+	if dryRun {
+		data, err := a.spec.Marshal()
+		if err != nil {
+			return err
+		}
+
+		fmt.Fprintf(a.out, "\n[dry run] Upgraded app.yaml:\n%s\n", string(data))
+		fmt.Fprintf(a.out, "[dry run] You can preform the migration by running `ks upgrade`.\n")
+		return nil
+	}
+
+	return a.save()
+}
+
+func (a *App001) convertEnvironment(envName string, dryRun bool) error {
+	path := filepath.Join(a.root, EnvironmentDirName, envName, "spec.json")
+	env, err := read001EnvSpec(a.fs, envName, path)
+	if err != nil {
+		return err
+	}
+
+	a.spec.Environments[envName] = env
+
+	if dryRun {
+		fmt.Fprintf(a.out, "[dry run]\t* adding the environment description in environment `%s to `app.yaml`.\n",
+			envName)
+		return nil
+	}
+
+	if err = a.fs.Remove(path); err != nil {
+		return err
+	}
+
+	k8sSpecFlag := fmt.Sprintf("version:%s", env.KubernetesVersion)
+	return LibUpdater(a.fs, k8sSpecFlag, app010LibPath(a.root), true)
+}
+
+func (a *App001) appLibPath(envName string) string {
+	return filepath.Join(a.root, EnvironmentDirName, envName, ".metadata")
+}
+
+func (a *App001) save() error {
+	return Write(a.fs, a.root, a.spec)
+}
+
+func (a *App001) load() error {
+	spec, err := Read(a.fs, a.root)
+	if err != nil {
+		return err
+	}
+
+	a.spec = spec
+	return nil
+}
+
+// LibPath returns the lib path for an env environment.
+func (a *App001) LibPath(envName string) (string, error) {
+	env, err := a.Environment(envName)
+	if err != nil {
+		return "", err
+	}
+
+	ver := fmt.Sprintf("version:%s", env.KubernetesVersion)
+	lm, err := lib.NewManager(ver, a.fs, a.appLibPath(envName))
+	if err != nil {
+		return "", err
+	}
+
+	return lm.GetLibPath(false)
+}
diff --git a/metadata/app/app001_test.go b/metadata/app/app001_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..d74d348e51887c379212a7c960733876b74f82a5
--- /dev/null
+++ b/metadata/app/app001_test.go
@@ -0,0 +1,204 @@
+package app
+
+import (
+	"bytes"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func TestApp001_Environments(t *testing.T) {
+	withApp001Fs(t, "app001_app.yaml", func(fs afero.Fs) {
+		app, err := NewApp001(fs, "/")
+		require.NoError(t, err)
+
+		expected := EnvironmentSpecs{
+			"default": &EnvironmentSpec{
+				Destination: &EnvironmentDestinationSpec{
+					Namespace: "some-namespace",
+					Server:    "http://example.com",
+				},
+				KubernetesVersion: "v1.7.0",
+				Path:              "default",
+			},
+			"us-east/test": &EnvironmentSpec{
+				Destination: &EnvironmentDestinationSpec{
+					Namespace: "some-namespace",
+					Server:    "http://example.com",
+				},
+				KubernetesVersion: "v1.7.0",
+				Path:              "us-east/test",
+			},
+			"us-west/test": &EnvironmentSpec{
+				Destination: &EnvironmentDestinationSpec{
+					Namespace: "some-namespace",
+					Server:    "http://example.com",
+				},
+				KubernetesVersion: "v1.7.0",
+				Path:              "us-west/test",
+			},
+			"us-west/prod": &EnvironmentSpec{
+				Destination: &EnvironmentDestinationSpec{
+					Namespace: "some-namespace",
+					Server:    "http://example.com",
+				},
+				KubernetesVersion: "v1.7.0",
+				Path:              "us-west/prod",
+			},
+		}
+		envs, err := app.Environments()
+		require.NoError(t, err)
+
+		require.Equal(t, expected, envs)
+	})
+}
+
+func TestApp001_Environment(t *testing.T) {
+	cases := []struct {
+		name    string
+		envName string
+		isErr   bool
+	}{
+		{
+			name:    "existing env",
+			envName: "us-east/test",
+		},
+		{
+			name:    "invalid env",
+			envName: "missing",
+			isErr:   true,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			withApp001Fs(t, "app001_app.yaml", func(fs afero.Fs) {
+				app, err := NewApp001(fs, "/")
+				require.NoError(t, err)
+
+				spec, err := app.Environment(tc.envName)
+				if tc.isErr {
+					require.Error(t, err)
+				} else {
+					require.NoError(t, err)
+					require.Equal(t, tc.envName, spec.Path)
+				}
+			})
+		})
+	}
+}
+
+func TestApp001_AddEnvironment(t *testing.T) {
+	withApp001Fs(t, "app001_app.yaml", func(fs afero.Fs) {
+		app, err := NewApp001(fs, "/")
+		require.NoError(t, err)
+
+		newEnv := &EnvironmentSpec{
+			Destination: &EnvironmentDestinationSpec{
+				Namespace: "some-namespace",
+				Server:    "http://example.com",
+			},
+			Path: "us-west/qa",
+		}
+
+		k8sSpecFlag := "version:v1.8.7"
+		err = app.AddEnvironment("us-west/qa", k8sSpecFlag, newEnv)
+		require.NoError(t, err)
+
+		_, err = app.Environment("us-west/qa")
+		require.NoError(t, err)
+	})
+}
+
+func TestApp001_Upgrade_dryrun(t *testing.T) {
+	withApp001Fs(t, "app001_app.yaml", func(fs afero.Fs) {
+		app, err := NewApp001(fs, "/")
+		require.NoError(t, err)
+
+		var buf bytes.Buffer
+		app.out = &buf
+
+		err = app.Upgrade(true)
+		require.NoError(t, err)
+
+		expected, err := ioutil.ReadFile("testdata/upgrade001.txt")
+		require.NoError(t, err)
+
+		require.Equal(t, string(expected), buf.String())
+	})
+}
+
+func TestApp001_Upgrade(t *testing.T) {
+	withApp001Fs(t, "app001_app.yaml", func(fs afero.Fs) {
+		app, err := NewApp001(fs, "/")
+		require.NoError(t, err)
+
+		var buf bytes.Buffer
+		app.out = &buf
+
+		err = app.Upgrade(false)
+		require.NoError(t, err)
+
+		root := filepath.Join(app.root, EnvironmentDirName)
+		var foundSpec bool
+		err = afero.Walk(fs, root, func(path string, fi os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+
+			if fi.IsDir() {
+				return nil
+			}
+
+			if fi.Name() == "spec.json" {
+				foundSpec = true
+			}
+			return nil
+		})
+
+		require.NoError(t, err)
+		require.False(t, foundSpec)
+	})
+}
+
+func withApp001Fs(t *testing.T, appName string, fn func(fs afero.Fs)) {
+	ogLibUpdater := LibUpdater
+	LibUpdater = func(fs afero.Fs, k8sSpecFlag string, libPath string, useVersionPath bool) error {
+		path := filepath.Join(libPath, "swagger.json")
+		stageFile(t, fs, "swagger.json", path)
+		return nil
+	}
+
+	defer func() {
+		LibUpdater = ogLibUpdater
+	}()
+
+	fs := afero.NewMemMapFs()
+
+	envDirs := []string{
+		"default",
+		"us-east/test",
+		"us-west/test",
+		"us-west/prod",
+	}
+
+	for _, dir := range envDirs {
+		path := filepath.Join("/environments", dir)
+		err := fs.MkdirAll(path, DefaultFolderPermissions)
+		require.NoError(t, err)
+
+		specPath := filepath.Join(path, "spec.json")
+		stageFile(t, fs, "spec.json", specPath)
+
+		swaggerPath := filepath.Join(path, ".metadata", "swagger.json")
+		stageFile(t, fs, "swagger.json", swaggerPath)
+	}
+
+	stageFile(t, fs, appName, "/app.yaml")
+
+	fn(fs)
+}
diff --git a/metadata/app/app010.go b/metadata/app/app010.go
new file mode 100644
index 0000000000000000000000000000000000000000..0cf90da6c16efd4f66e0f58d28d3e2043396984b
--- /dev/null
+++ b/metadata/app/app010.go
@@ -0,0 +1,169 @@
+package app
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/ksonnet/ksonnet/metadata/lib"
+
+	"github.com/pkg/errors"
+	"github.com/spf13/afero"
+)
+
+// App010 is a ksonnet 0.1.0 application.
+type App010 struct {
+	spec *Spec
+	root string
+	fs   afero.Fs
+}
+
+var _ App = (*App010)(nil)
+
+// NewApp010 creates an App010 instance.
+func NewApp010(fs afero.Fs, root string) (*App010, error) {
+	a := &App010{
+		fs:   fs,
+		root: root,
+	}
+
+	if err := a.load(); err != nil {
+		return nil, err
+	}
+
+	return a, nil
+}
+
+// Init initializes the App.
+func (a *App010) Init() error {
+	// check to see if there are spec.json files.
+
+	legacyEnvs, err := a.findLegacySpec()
+	if err != nil {
+		return err
+	}
+
+	if len(legacyEnvs) == 0 {
+		return nil
+	}
+
+	msg := "Your application's apiVersion is 0.1.0, but legacy environment declarations " +
+		"where found in environments: %s. In order to proceed, you will have to run `ks upgrade` to " +
+		"upgrade your application. <see url>"
+
+	return errors.Errorf(msg, strings.Join(legacyEnvs, ", "))
+}
+
+func (a *App010) findLegacySpec() ([]string, error) {
+	var found []string
+
+	envPath := filepath.Join(a.root, EnvironmentDirName)
+	err := afero.Walk(a.fs, envPath, func(path string, fi os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if fi.IsDir() {
+			return nil
+		}
+
+		if fi.Name() == app001specJSON {
+			envName := strings.TrimPrefix(path, envPath+"/")
+			envName = strings.TrimSuffix(envName, "/"+app001specJSON)
+			found = append(found, envName)
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return found, nil
+}
+
+// AddEnvironment adds an environment spec to the app spec. If the spec already exists,
+// it is overwritten.
+func (a *App010) AddEnvironment(name, k8sSpecFlag string, spec *EnvironmentSpec) error {
+	if err := a.load(); err != nil {
+		return err
+	}
+
+	a.spec.Environments[name] = spec
+
+	if err := LibUpdater(a.fs, k8sSpecFlag, app010LibPath(a.root), true); err != nil {
+		return err
+	}
+
+	return a.save()
+}
+
+// Registries returns application registries.
+func (a *App010) Registries() RegistryRefSpecs {
+	return a.spec.Registries
+}
+
+// Libraries returns application libraries.
+func (a *App010) Libraries() LibraryRefSpecs {
+	return a.spec.Libraries
+}
+
+// Environment returns the spec for an environment.
+func (a *App010) Environment(name string) (*EnvironmentSpec, error) {
+	s, ok := a.spec.Environments[name]
+	if !ok {
+		return nil, errors.Errorf("environment %q was not found", name)
+	}
+
+	return s, nil
+}
+
+// Environments returns all environment specs.
+func (a *App010) Environments() (EnvironmentSpecs, error) {
+	return a.spec.Environments, nil
+}
+
+// RemoveEnvironment removes an environment.
+func (a *App010) RemoveEnvironment(envName string) error {
+	if err := a.load(); err != nil {
+		return err
+	}
+	delete(a.spec.Environments, envName)
+	return a.save()
+}
+
+func (a *App010) save() error {
+	return Write(a.fs, a.root, a.spec)
+}
+
+func (a *App010) load() error {
+	spec, err := Read(a.fs, a.root)
+	if err != nil {
+		return err
+	}
+
+	a.spec = spec
+	return nil
+}
+
+// Upgrade upgrades the app to the latest apiVersion.
+func (a *App010) Upgrade(dryRun bool) error {
+	return nil
+}
+
+// LibPath returns the lib path for an env environment.
+func (a *App010) LibPath(envName string) (string, error) {
+	env, err := a.Environment(envName)
+	if err != nil {
+		return "", err
+	}
+
+	ver := fmt.Sprintf("version:%s", env.KubernetesVersion)
+	lm, err := lib.NewManager(ver, a.fs, app010LibPath(a.root))
+	if err != nil {
+		return "", err
+	}
+
+	return lm.GetLibPath(true)
+}
diff --git a/metadata/app/app010_test.go b/metadata/app/app010_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..138ead17a178899ca867d23089282060a7fbdfa7
--- /dev/null
+++ b/metadata/app/app010_test.go
@@ -0,0 +1,168 @@
+package app
+
+import (
+	"io/ioutil"
+	"path/filepath"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func TestApp0101_Environments(t *testing.T) {
+	withApp010Fs(t, "app010_app.yaml", func(fs afero.Fs) {
+		app, err := NewApp010(fs, "/")
+		require.NoError(t, err)
+
+		expected := EnvironmentSpecs{
+			"default": &EnvironmentSpec{
+				Destination: &EnvironmentDestinationSpec{
+					Namespace: "some-namespace",
+					Server:    "http://example.com",
+				},
+				KubernetesVersion: "v1.7.0",
+				Path:              "default",
+			},
+			"us-east/test": &EnvironmentSpec{
+				Destination: &EnvironmentDestinationSpec{
+					Namespace: "some-namespace",
+					Server:    "http://example.com",
+				},
+				Path: "us-east/test",
+			},
+			"us-west/test": &EnvironmentSpec{
+				Destination: &EnvironmentDestinationSpec{
+					Namespace: "some-namespace",
+					Server:    "http://example.com",
+				},
+				Path: "us-west/test",
+			},
+			"us-west/prod": &EnvironmentSpec{
+				Destination: &EnvironmentDestinationSpec{
+					Namespace: "some-namespace",
+					Server:    "http://example.com",
+				},
+				Path: "us-west/prod",
+			},
+		}
+		envs, err := app.Environments()
+		require.NoError(t, err)
+
+		require.Equal(t, expected, envs)
+	})
+}
+
+func TestApp010_Environment(t *testing.T) {
+	cases := []struct {
+		name    string
+		envName string
+		isErr   bool
+	}{
+		{
+			name:    "existing env",
+			envName: "us-east/test",
+		},
+		{
+			name:    "invalid env",
+			envName: "missing",
+			isErr:   true,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			withApp010Fs(t, "app010_app.yaml", func(fs afero.Fs) {
+				app, err := NewApp010(fs, "/")
+				require.NoError(t, err)
+
+				spec, err := app.Environment(tc.envName)
+				if tc.isErr {
+					require.Error(t, err)
+				} else {
+					require.NoError(t, err)
+					require.Equal(t, tc.envName, spec.Path)
+				}
+			})
+		})
+	}
+}
+
+func TestApp010_AddEnvironment(t *testing.T) {
+	withApp010Fs(t, "app010_app.yaml", func(fs afero.Fs) {
+		app, err := NewApp010(fs, "/")
+		require.NoError(t, err)
+
+		envs, err := app.Environments()
+		require.NoError(t, err)
+
+		envLen := len(envs)
+
+		newEnv := &EnvironmentSpec{
+			Destination: &EnvironmentDestinationSpec{
+				Namespace: "some-namespace",
+				Server:    "http://example.com",
+			},
+			Path: "us-west/qa",
+		}
+
+		k8sSpecFlag := "version:v1.8.7"
+		err = app.AddEnvironment("us-west/qa", k8sSpecFlag, newEnv)
+		require.NoError(t, err)
+
+		envs, err = app.Environments()
+		require.NoError(t, err)
+		require.Len(t, envs, envLen+1)
+
+		_, err = app.Environment("us-west/qa")
+		require.NoError(t, err)
+	})
+}
+
+func TestApp010_RemoveEnvironment(t *testing.T) {
+	withApp010Fs(t, "app010_app.yaml", func(fs afero.Fs) {
+		app, err := NewApp010(fs, "/")
+		require.NoError(t, err)
+
+		_, err = app.Environment("default")
+		require.NoError(t, err)
+
+		err = app.RemoveEnvironment("default")
+		require.NoError(t, err)
+
+		app, err = NewApp010(fs, "/")
+		require.NoError(t, err)
+
+		_, err = app.Environment("default")
+		require.Error(t, err)
+	})
+}
+
+func withApp010Fs(t *testing.T, appName string, fn func(fs afero.Fs)) {
+	ogLibUpdater := LibUpdater
+	LibUpdater = func(fs afero.Fs, k8sSpecFlag string, libPath string, useVersionPath bool) error {
+		return nil
+	}
+
+	defer func() {
+		LibUpdater = ogLibUpdater
+	}()
+
+	fs := afero.NewMemMapFs()
+	stageFile(t, fs, appName, "/app.yaml")
+
+	fn(fs)
+}
+
+func stageFile(t *testing.T, fs afero.Fs, src, dest string) {
+	in := filepath.Join("testdata", src)
+
+	b, err := ioutil.ReadFile(in)
+	require.NoError(t, err)
+
+	dir := filepath.Dir(dest)
+	err = fs.MkdirAll(dir, 0755)
+	require.NoError(t, err)
+
+	err = afero.WriteFile(fs, dest, b, 0644)
+	require.NoError(t, err)
+}
diff --git a/metadata/app/mocks/App.go b/metadata/app/mocks/App.go
new file mode 100644
index 0000000000000000000000000000000000000000..898067f2c048b38e0afd84b7980d2e10ed76bb6a
--- /dev/null
+++ b/metadata/app/mocks/App.go
@@ -0,0 +1,165 @@
+// Code generated by mockery v1.0.0
+package mocks
+
+import app "github.com/ksonnet/ksonnet/metadata/app"
+import mock "github.com/stretchr/testify/mock"
+
+// App is an autogenerated mock type for the App type
+type App struct {
+	mock.Mock
+}
+
+// AddEnvironment provides a mock function with given fields: name, k8sSpecFlag, spec
+func (_m *App) AddEnvironment(name string, k8sSpecFlag string, spec *app.EnvironmentSpec) error {
+	ret := _m.Called(name, k8sSpecFlag, spec)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(string, string, *app.EnvironmentSpec) error); ok {
+		r0 = rf(name, k8sSpecFlag, spec)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Environment provides a mock function with given fields: name
+func (_m *App) Environment(name string) (*app.EnvironmentSpec, error) {
+	ret := _m.Called(name)
+
+	var r0 *app.EnvironmentSpec
+	if rf, ok := ret.Get(0).(func(string) *app.EnvironmentSpec); ok {
+		r0 = rf(name)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*app.EnvironmentSpec)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(name)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Environments provides a mock function with given fields:
+func (_m *App) Environments() (app.EnvironmentSpecs, error) {
+	ret := _m.Called()
+
+	var r0 app.EnvironmentSpecs
+	if rf, ok := ret.Get(0).(func() app.EnvironmentSpecs); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(app.EnvironmentSpecs)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func() error); ok {
+		r1 = rf()
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Init provides a mock function with given fields:
+func (_m *App) Init() error {
+	ret := _m.Called()
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func() error); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// LibPath provides a mock function with given fields: envName
+func (_m *App) LibPath(envName string) (string, error) {
+	ret := _m.Called(envName)
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func(string) string); ok {
+		r0 = rf(envName)
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(envName)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Libraries provides a mock function with given fields:
+func (_m *App) Libraries() app.LibraryRefSpecs {
+	ret := _m.Called()
+
+	var r0 app.LibraryRefSpecs
+	if rf, ok := ret.Get(0).(func() app.LibraryRefSpecs); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(app.LibraryRefSpecs)
+		}
+	}
+
+	return r0
+}
+
+// Registries provides a mock function with given fields:
+func (_m *App) Registries() app.RegistryRefSpecs {
+	ret := _m.Called()
+
+	var r0 app.RegistryRefSpecs
+	if rf, ok := ret.Get(0).(func() app.RegistryRefSpecs); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(app.RegistryRefSpecs)
+		}
+	}
+
+	return r0
+}
+
+// RemoveEnvironment provides a mock function with given fields: name
+func (_m *App) RemoveEnvironment(name string) error {
+	ret := _m.Called(name)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(string) error); ok {
+		r0 = rf(name)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Upgrade provides a mock function with given fields: dryRun
+func (_m *App) Upgrade(dryRun bool) error {
+	ret := _m.Called(dryRun)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(bool) error); ok {
+		r0 = rf(dryRun)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
diff --git a/metadata/app/schema.go b/metadata/app/schema.go
index 1e4899098e28a859ba4afc1ba2899db3ba11567a..12c4a36ea9cd20bc58007f478a999cab929828a1 100644
--- a/metadata/app/schema.go
+++ b/metadata/app/schema.go
@@ -17,15 +17,17 @@ package app
 
 import (
 	"fmt"
+	"path/filepath"
 
 	"github.com/blang/semver"
 	"github.com/ghodss/yaml"
 	"github.com/pkg/errors"
+	"github.com/spf13/afero"
 )
 
 const (
 	// DefaultAPIVersion is the default ks API version to use if not specified.
-	DefaultAPIVersion = "0.0.1"
+	DefaultAPIVersion = "0.1.0"
 	// Kind is the schema resource type.
 	Kind = "ksonnet.io/app"
 	// DefaultVersion is the default version of the app schema.
@@ -64,6 +66,51 @@ type Spec struct {
 	License      string           `json:"license,omitempty"`
 }
 
+// Read will return the specification for a ksonnet application.
+func Read(fs afero.Fs, appRoot string) (*Spec, error) {
+	bytes, err := afero.ReadFile(fs, specPath(appRoot))
+	if err != nil {
+		return nil, err
+	}
+
+	schema, err := Unmarshal(bytes)
+	if err != nil {
+		return nil, err
+	}
+
+	if schema.Contributors == nil {
+		schema.Contributors = ContributorSpecs{}
+	}
+
+	if schema.Registries == nil {
+		schema.Registries = RegistryRefSpecs{}
+	}
+
+	if schema.Libraries == nil {
+		schema.Libraries = LibraryRefSpecs{}
+	}
+
+	if schema.Environments == nil {
+		schema.Environments = EnvironmentSpecs{}
+	}
+
+	return schema, nil
+}
+
+// Write writes the provided spec to file system.
+func Write(fs afero.Fs, appRoot string, spec *Spec) error {
+	data, err := spec.Marshal()
+	if err != nil {
+		return err
+	}
+
+	return afero.WriteFile(fs, specPath(appRoot), data, DefaultFilePermissions)
+}
+
+func specPath(appRoot string) string {
+	return filepath.Join(appRoot, appYamlName)
+}
+
 // RepositorySpec defines the spec for the upstream repository of this project.
 type RepositorySpec struct {
 	Type string `json:"type"`
@@ -185,11 +232,17 @@ func (s *Spec) AddRegistryRef(registryRefSpec *RegistryRefSpec) error {
 }
 
 func (s *Spec) validate() error {
+	if s.APIVersion == "0.0.0" {
+		return errors.New("invalid version")
+	}
+
 	compatVer, _ := semver.Make(DefaultAPIVersion)
 	ver, err := semver.Make(s.APIVersion)
 	if err != nil {
 		return errors.Wrap(err, "Failed to parse version in app spec")
-	} else if compatVer.Compare(ver) != 0 {
+	}
+
+	if compatVer.Compare(ver) < 0 {
 		return fmt.Errorf(
 			"Current app uses unsupported spec version '%s' (this client only supports %s)",
 			s.APIVersion,
@@ -249,7 +302,7 @@ func (s *Spec) UpdateEnvironmentSpec(name string, spec *EnvironmentSpec) error {
 
 	_, environmentSpecExists := s.Environments[name]
 	if !environmentSpecExists {
-		return ErrEnvironmentNotExists
+		return errors.Errorf("Environment with name %q does not exist", name)
 	}
 
 	if name != spec.Name {
diff --git a/metadata/app/schema_test.go b/metadata/app/schema_test.go
index 4b6cd394349e370a9239f13ea401effcf7bef2c1..a7d2badd56228d630f5c380ee42c82f49f124f0c 100644
--- a/metadata/app/schema_test.go
+++ b/metadata/app/schema_test.go
@@ -53,23 +53,13 @@ func TestApiVersionValidate(t *testing.T) {
 		// Versions that we accept.
 		{spec: "0.0.1", err: false},
 		{spec: "0.0.1+build.1", err: false},
+		{spec: "0.1.0-alpha", err: false},
+		{spec: "0.1.0+build.1"},
 
 		// Other versions.
 		{spec: "0.0.0", err: true},
-		{spec: "0.1.0", err: true},
+		{spec: "0.1.0"},
 		{spec: "1.0.0", err: true},
-
-		// Builds and pre-releases of current version.
-		{spec: "0.0.1-alpha", err: true},
-		{spec: "0.0.1-beta+build.2", err: true},
-
-		// Other versions.
-		{spec: "0.1.0-alpha", err: true},
-		{spec: "0.1.0+build.1", err: true},
-		{spec: "0.1.0-beta+build.2", err: true},
-		{spec: "1.0.0-alpha", err: true},
-		{spec: "1.0.0+build.1", err: true},
-		{spec: "1.0.0-beta+build.2", err: true},
 	}
 
 	for _, test := range tests {
diff --git a/metadata/app/testdata/app001_app.yaml b/metadata/app/testdata/app001_app.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9ec75175c9107898854186ea65284c6f67ed61fa
--- /dev/null
+++ b/metadata/app/testdata/app001_app.yaml
@@ -0,0 +1,9 @@
+apiVersion: 0.1.0
+kind: ksonnet.io/app
+name: test-get-envs
+registries:
+  incubator:
+    gitVersion: null
+    protocol: ""
+    uri: ""
+version: 0.0.1
\ No newline at end of file
diff --git a/metadata/app/testdata/app010_app.yaml b/metadata/app/testdata/app010_app.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c30e5fe5f2fdcaaccdeaa65cbfe09b7a4778052f
--- /dev/null
+++ b/metadata/app/testdata/app010_app.yaml
@@ -0,0 +1,34 @@
+apiVersion: 0.1.0
+environments:
+  default:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: v1.7.0
+    path: default
+  us-east/test:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: ""
+    path: us-east/test
+  us-west/prod:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: ""
+    path: us-west/prod
+  us-west/test:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: ""
+    path: us-west/test
+kind: ksonnet.io/app
+name: test-get-envs
+registries:
+  incubator:
+    gitVersion: null
+    protocol: ""
+    uri: ""
+version: 0.0.1
\ No newline at end of file
diff --git a/metadata/app/testdata/spec.json b/metadata/app/testdata/spec.json
new file mode 100644
index 0000000000000000000000000000000000000000..945acc462b9a9991fd8c5ac112bf996abb009c1a
--- /dev/null
+++ b/metadata/app/testdata/spec.json
@@ -0,0 +1,4 @@
+{
+    "namespace": "some-namespace",
+    "server": "http://example.com"
+}
\ No newline at end of file
diff --git a/metadata/app/testdata/swagger.json b/metadata/app/testdata/swagger.json
new file mode 100644
index 0000000000000000000000000000000000000000..ff4ba3b9c97202df111cc37714ddd3c534c23e2a
--- /dev/null
+++ b/metadata/app/testdata/swagger.json
@@ -0,0 +1,5 @@
+{
+    "info": {
+        "version": "v1.7.0"
+    }
+}
\ No newline at end of file
diff --git a/metadata/app/testdata/upgrade001.txt b/metadata/app/testdata/upgrade001.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2b103976b2b6457c7d39df98440aa63bf33f7259
--- /dev/null
+++ b/metadata/app/testdata/upgrade001.txt
@@ -0,0 +1,45 @@
+
+[dry run] Upgrading application settings from version 0.0.1 to to 0.1.0.
+[dry run] Converting 0.0.1 environments to 0.1.0a:
+[dry run]	* adding the environment description in environment `default to `app.yaml`.
+[dry run]	* adding the environment description in environment `us-east/test to `app.yaml`.
+[dry run]	* adding the environment description in environment `us-west/prod to `app.yaml`.
+[dry run]	* adding the environment description in environment `us-west/test to `app.yaml`.
+
+[dry run] Upgraded app.yaml:
+apiVersion: 0.1.0
+environments:
+  default:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: v1.7.0
+    path: default
+  us-east/test:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: v1.7.0
+    path: us-east/test
+  us-west/prod:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: v1.7.0
+    path: us-west/prod
+  us-west/test:
+    destination:
+      namespace: some-namespace
+      server: http://example.com
+    k8sVersion: v1.7.0
+    path: us-west/test
+kind: ksonnet.io/app
+name: test-get-envs
+registries:
+  incubator:
+    gitVersion: null
+    protocol: ""
+    uri: ""
+version: 0.0.1
+
+[dry run] You can preform the migration by running `ks upgrade`.
diff --git a/metadata/component.go b/metadata/component.go
index 7cf8eacaa5deafa36fbf53ab22edf405d1b161b5..79648a362ee82b74d986fb81e8355f1c5a2f1b5e 100644
--- a/metadata/component.go
+++ b/metadata/component.go
@@ -157,7 +157,6 @@ func (m *manager) DeleteComponent(name string) error {
 }
 
 func (m *manager) GetComponentParams(component string) (param.Params, error) {
-	log.Infof("get component params for %s", component)
 	text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
 	if err != nil {
 		return nil, err
diff --git a/metadata/component_test.go b/metadata/component_test.go
index 5693508d3cbfc86c12f0d819044c8a190cb404f6..8e0ef53ebf2333f226a45a5e2dc899e783a441cb 100644
--- a/metadata/component_test.go
+++ b/metadata/component_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 
 	str "github.com/ksonnet/ksonnet/strings"
+	"github.com/spf13/afero"
 )
 
 const (
@@ -31,12 +32,12 @@ const (
 	componentFile2  = "component2.jsonnet"
 )
 
-func populateComponentPaths(t *testing.T) *manager {
+func populateComponentPaths(t *testing.T, fs afero.Fs) *manager {
 	specFlag := fmt.Sprintf("file:%s", blankSwagger)
 
 	appPath := componentsPath
 	reg := newMockRegistryManager("incubator")
-	m, err := initManager("componentPaths", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
+	m, err := initManager("componentPaths", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, fs)
 	if err != nil {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
@@ -44,7 +45,7 @@ func populateComponentPaths(t *testing.T) *manager {
 	// Create empty app file.
 	components := str.AppendToPath(appPath, componentsDir)
 	appFile1 := str.AppendToPath(components, componentFile1)
-	f1, err := testFS.OpenFile(appFile1, os.O_RDONLY|os.O_CREATE, 0777)
+	f1, err := fs.OpenFile(appFile1, os.O_RDONLY|os.O_CREATE, 0777)
 	if err != nil {
 		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
 	}
@@ -52,12 +53,12 @@ func populateComponentPaths(t *testing.T) *manager {
 
 	// Create empty file in a nested directory.
 	appSubdir := str.AppendToPath(components, componentSubdir)
-	err = testFS.MkdirAll(appSubdir, os.ModePerm)
+	err = fs.MkdirAll(appSubdir, os.ModePerm)
 	if err != nil {
 		t.Fatalf("Failed to create directory '%s'\n%v", appSubdir, err)
 	}
 	appFile2 := str.AppendToPath(appSubdir, componentFile2)
-	f2, err := testFS.OpenFile(appFile2, os.O_RDONLY|os.O_CREATE, 0777)
+	f2, err := fs.OpenFile(appFile2, os.O_RDONLY|os.O_CREATE, 0777)
 	if err != nil {
 		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
 	}
@@ -65,7 +66,7 @@ func populateComponentPaths(t *testing.T) *manager {
 
 	// Create a directory that won't be listed in the call to `ComponentPaths`.
 	unlistedDir := str.AppendToPath(components, "doNotListMe")
-	err = testFS.MkdirAll(unlistedDir, os.ModePerm)
+	err = fs.MkdirAll(unlistedDir, os.ModePerm)
 	if err != nil {
 		t.Fatalf("Failed to create directory '%s'\n%v", unlistedDir, err)
 	}
@@ -73,47 +74,44 @@ func populateComponentPaths(t *testing.T) *manager {
 	return m
 }
 
-func cleanComponentPaths(t *testing.T) {
-	testFS.RemoveAll(componentsPath)
-}
-
 func TestComponentPaths(t *testing.T) {
-	m := populateComponentPaths(t)
-	defer cleanComponentPaths(t)
+	withFs(func(fs afero.Fs) {
+		m := populateComponentPaths(t, fs)
 
-	paths, err := m.ComponentPaths()
-	if err != nil {
-		t.Fatalf("Failed to find component paths: %v", err)
-	}
+		paths, err := m.ComponentPaths()
+		if err != nil {
+			t.Fatalf("Failed to find component paths: %v", err)
+		}
 
-	sort.Slice(paths, func(i, j int) bool { return paths[i] < paths[j] })
+		sort.Slice(paths, func(i, j int) bool { return paths[i] < paths[j] })
 
-	expectedPath1 := fmt.Sprintf("%s/components/%s", componentsPath, componentFile1)
-	expectedPath2 := fmt.Sprintf("%s/components/%s/%s", componentsPath, componentSubdir, componentFile2)
+		expectedPath1 := fmt.Sprintf("%s/components/%s", componentsPath, componentFile1)
+		expectedPath2 := fmt.Sprintf("%s/components/%s/%s", componentsPath, componentSubdir, componentFile2)
 
-	if len(paths) != 2 || paths[0] != expectedPath1 || paths[1] != expectedPath2 {
-		t.Fatalf("m.ComponentPaths failed; expected '%s', got '%s'", []string{expectedPath1, expectedPath2}, paths)
-	}
+		if len(paths) != 2 || paths[0] != expectedPath1 || paths[1] != expectedPath2 {
+			t.Fatalf("m.ComponentPaths failed; expected '%s', got '%s'", []string{expectedPath1, expectedPath2}, paths)
+		}
+	})
 }
 
 // TODO: this logic and tests should be moved to the components namespace.
 func TestGetAllComponents(t *testing.T) {
-	m := populateComponentPaths(t)
-	defer cleanComponentPaths(t)
+	withFs(func(fs afero.Fs) {
+		m := populateComponentPaths(t, fs)
 
-	components, err := m.GetAllComponents()
-	if err != nil {
-		t.Fatalf("Failed to get all components, %v", err)
-	}
+		components, err := m.GetAllComponents()
+		if err != nil {
+			t.Fatalf("Failed to get all components, %v", err)
+		}
 
-	expected1 := strings.TrimSuffix(componentFile1, ".jsonnet")
+		expected1 := strings.TrimSuffix(componentFile1, ".jsonnet")
 
-	if len(components) != 1 {
-		t.Fatalf("Expected exactly 1 components, got %d", len(components))
-	}
-
-	if components[0] != expected1 {
-		t.Fatalf("Expected component %s, got %s", expected1, components)
-	}
+		if len(components) != 1 {
+			t.Fatalf("Expected exactly 1 components, got %d", len(components))
+		}
 
+		if components[0] != expected1 {
+			t.Fatalf("Expected component %s, got %s", expected1, components)
+		}
+	})
 }
diff --git a/metadata/environment.go b/metadata/environment.go
index d00fe04c324a6c1ae57ca0bb04fa2383f2c0ed2f..92be02b7c9ef3fb756c19ce29dd941ae59fe2c93 100644
--- a/metadata/environment.go
+++ b/metadata/environment.go
@@ -18,17 +18,12 @@ package metadata
 import (
 	"bytes"
 	"fmt"
-	"os"
-	"path"
-	"path/filepath"
 
-	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/ksonnet/ksonnet/env"
 	"github.com/ksonnet/ksonnet/metadata/lib"
 	str "github.com/ksonnet/ksonnet/strings"
-	"github.com/pkg/errors"
 
 	log "github.com/sirupsen/logrus"
-	"github.com/spf13/afero"
 
 	param "github.com/ksonnet/ksonnet/metadata/params"
 )
@@ -41,333 +36,98 @@ const (
 	paramsFileName = "params.libsonnet"
 )
 
-var envPaths = []string{
-	// environment base override file
-	envFileName,
-	// params file
-	paramsFileName,
-}
+var (
+	// envCreate is a function which creates environments.
+	envCreate = env.Create
+)
 
 func (m *manager) CreateEnvironment(name, server, namespace, k8sSpecFlag string) error {
-	// generate the lib data for this kubernetes version
-	libManager, err := lib.NewManager(k8sSpecFlag, m.appFS, m.libPath)
-	if err != nil {
-		return err
-	}
-
-	if err := libManager.GenerateLibData(); err != nil {
-		return err
-	}
-
-	// add the environment to the app spec
-	appSpec, err := m.AppSpec()
-	if err != nil {
-		return err
-	}
-
-	exists, err := m.environmentExists(name)
-	if err != nil {
-		return err
-	}
-	if exists {
-		return fmt.Errorf("Environment '%s' already exists", name)
-	}
-
-	// ensure environment name does not contain punctuation
-	if !isValidName(name) {
-		return fmt.Errorf("Environment name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
-	}
-
-	if namespace == "" {
-		namespace = "default"
-	}
-
-	log.Infof("Creating environment '%s' with namespace '%s', pointing at server at address '%s'", name, namespace, server)
-
-	envPath := str.AppendToPath(m.environmentsPath, name)
-	err = m.appFS.MkdirAll(envPath, defaultFolderPermissions)
+	a, err := m.App()
 	if err != nil {
 		return err
 	}
 
-	metadata := []struct {
-		path string
-		data []byte
-	}{
-		{
-			// environment base override file
-			str.AppendToPath(envPath, envFileName),
-			m.generateOverrideData(),
-		},
-		{
-			// params file
-			str.AppendToPath(envPath, paramsFileName),
-			m.generateParamsData(),
-		},
-	}
+	config := env.CreateConfig{
+		App:         a,
+		Destination: env.NewDestination(server, namespace),
+		Fs:          m.appFS,
+		K8sSpecFlag: k8sSpecFlag,
+		Name:        name,
+		RootPath:    m.rootPath,
 
-	for _, a := range metadata {
-		fileName := path.Base(a.path)
-		log.Debugf("Generating '%s', length: %d", fileName, len(a.data))
-		if err = afero.WriteFile(m.appFS, a.path, a.data, defaultFilePermissions); err != nil {
-			log.Debugf("Failed to write '%s'", fileName)
-			return err
-		}
+		OverrideData: m.generateOverrideData(),
+		ParamsData:   m.generateParamsData(),
 	}
 
-	// update app.yaml
-	err = appSpec.AddEnvironmentSpec(&app.EnvironmentSpec{
-		Name: name,
-		Path: name,
-		Destination: &app.EnvironmentDestinationSpec{
-			Server:    server,
-			Namespace: namespace,
-		},
-		KubernetesVersion: libManager.K8sVersion,
-	})
-
-	if err != nil {
-		return err
-	}
+	return envCreate(config)
 
-	return m.WriteAppSpec(appSpec)
 }
 
 func (m *manager) DeleteEnvironment(name string) error {
-	app, err := m.AppSpec()
-	if err != nil {
-		return err
-	}
-
-	env, err := m.GetEnvironment(name)
+	a, err := m.App()
 	if err != nil {
 		return err
 	}
 
-	envPath := str.AppendToPath(m.environmentsPath, env.Path)
-
-	log.Infof("Deleting environment '%s' with metadata at path '%s'", name, envPath)
-
-	// Remove the directory and all files within the environment path.
-	err = m.appFS.RemoveAll(envPath)
-	if err != nil {
-		log.Debugf("Failed to remove environment directory at path '%s'", envPath)
-		return err
+	config := env.DeleteConfig{
+		App:     a,
+		AppRoot: m.rootPath,
+		Name:    name,
+		Fs:      m.appFS,
 	}
-
-	// Need to ensure empty parent directories are also removed.
-	log.Debug("Removing empty parent directories, if any")
-	err = m.cleanEmptyParentDirs(name)
-	if err != nil {
-		return err
-	}
-
-	// Update app spec.
-	if err := m.WriteAppSpec(app); err != nil {
-		return err
-	}
-
-	log.Infof("Successfully removed environment '%s'", name)
-	return nil
+	return env.Delete(config)
 }
 
-func (m *manager) GetEnvironments() (app.EnvironmentSpecs, error) {
-	if err := m.errorOnSpecFile(); err != nil {
-		return nil, err
-	}
-
-	app, err := m.AppSpec()
+func (m *manager) GetEnvironments() (map[string]env.Env, error) {
+	a, err := m.App()
 	if err != nil {
 		return nil, err
 	}
 
 	log.Debug("Retrieving all environments")
-	return app.GetEnvironmentSpecs(), nil
+	return env.List(a)
 }
 
-func (m *manager) GetEnvironment(name string) (*app.EnvironmentSpec, error) {
-	if err := m.errorOnSpecFile(); err != nil {
-		return nil, err
-	}
-
-	app, err := m.AppSpec()
+func (m *manager) GetEnvironment(name string) (*env.Env, error) {
+	a, err := m.App()
 	if err != nil {
 		return nil, err
 	}
 
-	env, ok := app.GetEnvironmentSpec(name)
-	if !ok {
-		return nil, fmt.Errorf("Environment '%s' does not exist", name)
-	}
-
-	return env, nil
+	return env.Retrieve(a, name)
 }
 
-func (m *manager) SetEnvironment(name, desiredName string) error {
-	if name == desiredName || len(desiredName) == 0 {
-		return nil
-	}
-
-	// ensure new environment name does not contain punctuation
-	if !isValidName(desiredName) {
-		return fmt.Errorf("Environment name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
-	}
-
-	// Ensure not overwriting another environment
-	desiredExists, err := m.environmentExists(desiredName)
+func (m *manager) SetEnvironment(from, to string) error {
+	a, err := m.App()
 	if err != nil {
-		log.Debugf("Failed to check whether environment '%s' already exists", desiredName)
 		return err
 	}
-	if desiredExists {
-		return fmt.Errorf("Failed to update '%s'; environment '%s' exists", name, desiredName)
-	}
-
-	log.Infof("Setting environment name from '%s' to '%s'", name, desiredName)
-
-	//
-	// Update app spec. We will write out the app spec changes once all file
-	// move operations are complete.
-	//
 
-	appSpec, err := m.AppSpec()
-	if err != nil {
-		return err
+	config := env.RenameConfig{
+		App:     a,
+		AppRoot: m.rootPath,
+		Fs:      m.appFS,
 	}
 
-	current, err := m.GetEnvironment(name)
-	if err != nil {
-		return err
-	}
-
-	err = appSpec.UpdateEnvironmentSpec(name, &app.EnvironmentSpec{
-		Name:              desiredName,
-		Destination:       current.Destination,
-		KubernetesVersion: current.KubernetesVersion,
-		Targets:           current.Targets,
-		Path:              desiredName,
-	})
-
-	if err != nil {
-		return err
-	}
-
-	//
-	// If the name has changed, the directory location needs to be moved to
-	// reflect the change.
-	//
-
-	pathOld := str.AppendToPath(m.environmentsPath, name)
-	pathNew := str.AppendToPath(m.environmentsPath, desiredName)
-	exists, err := afero.DirExists(m.appFS, pathNew)
-	if err != nil {
-		return err
-	}
-
-	if exists {
-		// we know that the desired path is not an environment from
-		// the check earlier. This is an intermediate directory.
-		// We need to move the file contents.
-		m.tryMvEnvDir(pathOld, pathNew)
-	} else if filepath.HasPrefix(pathNew, pathOld) {
-		// the new directory is a child of the old directory --
-		// rename won't work.
-		err = m.appFS.MkdirAll(pathNew, defaultFolderPermissions)
-		if err != nil {
-			return err
-		}
-		m.tryMvEnvDir(pathOld, pathNew)
-	} else {
-		// Need to first create subdirectories that don't exist
-		intermediatePath := path.Dir(pathNew)
-		log.Debugf("Moving directory at path '%s' to '%s'", pathOld, pathNew)
-		err = m.appFS.MkdirAll(intermediatePath, defaultFolderPermissions)
-		if err != nil {
-			return err
-		}
-		// finally, move the directory
-		err = m.appFS.Rename(pathOld, pathNew)
-		if err != nil {
-			log.Debugf("Failed to move path '%s' to '%s", pathOld, pathNew)
-			return err
-		}
-	}
-
-	// clean up any empty parent directory paths
-	err = m.cleanEmptyParentDirs(name)
-	if err != nil {
-		return err
-	}
-
-	m.WriteAppSpec(appSpec)
-
-	log.Infof("Successfully updated environment '%s'", name)
-	return nil
+	return env.Rename(from, to, config)
 }
 
-func (m *manager) GetEnvironmentParams(name string) (map[string]param.Params, error) {
-	exists, err := m.environmentExists(name)
-	if err != nil {
-		return nil, err
-	}
-	if !exists {
-		return nil, fmt.Errorf("Environment '%s' does not exist", name)
+func (m *manager) GetEnvironmentParams(name, nsName string) (map[string]param.Params, error) {
+	config := env.GetParamsConfig{
+		AppRoot: m.rootPath,
+		Fs:      m.appFS,
 	}
 
-	// Get the environment specific params
-	envParamsPath := str.AppendToPath(m.environmentsPath, name, paramsFileName)
-	envParamsText, err := afero.ReadFile(m.appFS, envParamsPath)
-	if err != nil {
-		return nil, err
-	}
-	envParams, err := param.GetAllEnvironmentParams(string(envParamsText))
-	if err != nil {
-		return nil, err
-	}
-
-	cwd, err := os.Getwd()
-	if err != nil {
-		return nil, errors.Wrap(err, "get working directory")
-	}
-
-	// Get all component params
-	componentParams, err := m.GetAllComponentParams(cwd)
-	if err != nil {
-		return nil, err
-	}
-
-	// Merge the param sets, replacing the component params if the environment params override
-	return mergeParamMaps(componentParams, envParams), nil
+	return env.GetParams(name, nsName, config)
 }
 
-func (m *manager) SetEnvironmentParams(env, component string, params param.Params) error {
-	exists, err := m.environmentExists(env)
-	if err != nil {
-		return err
-	}
-	if !exists {
-		return fmt.Errorf("Environment '%s' does not exist", env)
-	}
-
-	path := str.AppendToPath(m.environmentsPath, env, paramsFileName)
-
-	text, err := afero.ReadFile(m.appFS, path)
-	if err != nil {
-		return err
-	}
-
-	appended, err := param.SetEnvironmentParams(component, string(text), params)
-	if err != nil {
-		return err
-	}
-
-	err = afero.WriteFile(m.appFS, path, []byte(appended), defaultFilePermissions)
-	if err != nil {
-		return err
+func (m *manager) SetEnvironmentParams(envName, component string, params param.Params) error {
+	config := env.SetParamsConfig{
+		AppRoot: m.rootPath,
+		Fs:      m.appFS,
 	}
 
-	log.Debugf("Successfully set parameters for component '%s' at environment '%s'", component, env)
-	return nil
+	return env.SetParams(envName, component, params, config)
 }
 
 func (m *manager) EnvPaths(env string) (libPath, mainPath, paramsPath string, err error) {
@@ -387,98 +147,13 @@ func (m *manager) makeEnvPaths(env string) (mainPath, paramsPath string) {
 	return
 }
 
-func (m *manager) getLibPath(env string) (string, error) {
-	envSpec, err := m.GetEnvironment(env)
+func (m *manager) getLibPath(envName string) (string, error) {
+	a, err := m.App()
 	if err != nil {
 		return "", err
 	}
 
-	libManager, err := lib.NewManager(fmt.Sprintf("version:%s", envSpec.KubernetesVersion), m.appFS, m.libPath)
-	if err != nil {
-		return "", err
-	}
-
-	return libManager.GetLibPath()
-}
-
-func (m *manager) errorOnSpecFile() error {
-	return afero.Walk(m.appFS, m.environmentsPath, func(p string, f os.FileInfo, err error) error {
-		if err != nil {
-			log.Debugf("Failed to walk path %s", p)
-			return err
-		}
-		isDir, err := afero.IsDir(m.appFS, p)
-		if err != nil {
-			log.Debugf("Failed to check whether the path at %s is a directory", p)
-			return err
-		}
-		if isDir {
-			specPath := filepath.Join(p, "spec.json")
-			specFileExists, err := afero.Exists(m.appFS, specPath)
-			if err != nil {
-				log.Debugf("Failed to check whether spec.json exists")
-				return err
-			}
-			if specFileExists {
-				// TODO, we should point users to a tutorial.
-				return fmt.Errorf("Environment's directory contains a dated model containing the 'spec.json' file. Please migrate to the new model by adding environments data to app.yaml")
-			}
-		}
-
-		return nil
-	})
-}
-
-func (m *manager) tryMvEnvDir(dirPathOld, dirPathNew string) error {
-	// first ensure none of these paths exists in the new directory
-	for _, p := range envPaths {
-		path := str.AppendToPath(dirPathNew, p)
-		if exists, err := afero.Exists(m.appFS, path); err != nil {
-			return err
-		} else if exists {
-			return fmt.Errorf("%s already exists", path)
-		}
-	}
-
-	// note: afero and go does not provide simple ways to move the
-	// contents. We'll have to rename them individually.
-	for _, p := range envPaths {
-		err := m.appFS.Rename(str.AppendToPath(dirPathOld, p), str.AppendToPath(dirPathNew, p))
-		if err != nil {
-			return err
-		}
-	}
-	// clean up the old directory if it is empty
-	if empty, err := afero.IsEmpty(m.appFS, dirPathOld); err != nil {
-		return err
-	} else if empty {
-		return m.appFS.RemoveAll(dirPathOld)
-	}
-	return nil
-}
-
-func (m *manager) cleanEmptyParentDirs(name string) error {
-	// clean up any empty parent directory paths
-	log.Debug("Removing empty parent directories, if any")
-	parentDir := name
-	for parentDir != "." {
-		parentDir = filepath.Dir(parentDir)
-		parentPath := str.AppendToPath(m.environmentsPath, parentDir)
-
-		isEmpty, err := afero.IsEmpty(m.appFS, parentPath)
-		if err != nil {
-			log.Debugf("Failed to check whether parent directory at path '%s' is empty", parentPath)
-			return err
-		}
-		if isEmpty {
-			log.Debugf("Failed to remove parent directory at path '%s'", parentPath)
-			err := m.appFS.RemoveAll(parentPath)
-			if err != nil {
-				return err
-			}
-		}
-	}
-	return nil
+	return a.LibPath(envName)
 }
 
 func (m *manager) generateOverrideData() []byte {
@@ -509,30 +184,3 @@ params + {
 }
 `)
 }
-
-func (m *manager) environmentExists(name string) (bool, error) {
-	appSpec, err := m.AppSpec()
-	if err != nil {
-		return false, err
-	}
-
-	if err := m.errorOnSpecFile(); err != nil {
-		return false, err
-	}
-
-	_, ok := appSpec.GetEnvironmentSpec(name)
-	return ok, nil
-}
-
-func mergeParamMaps(base, overrides map[string]param.Params) map[string]param.Params {
-	for component, params := range overrides {
-		if _, contains := base[component]; !contains {
-			base[component] = params
-		} else {
-			for k, v := range params {
-				base[component][k] = v
-			}
-		}
-	}
-	return base
-}
diff --git a/metadata/environment_test.go b/metadata/environment_test.go
index ef74fc111fcb955088a34be0a0c8902bbbe1624e..91f293c3a21308e71935aaa18bca6c078daa3bd9 100644
--- a/metadata/environment_test.go
+++ b/metadata/environment_test.go
@@ -17,14 +17,15 @@ package metadata
 
 import (
 	"fmt"
-	"reflect"
+	"path/filepath"
 	"strings"
 	"testing"
 
 	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/ksonnet/ksonnet/metadata/app/mocks"
 	str "github.com/ksonnet/ksonnet/strings"
+	"github.com/stretchr/testify/require"
 
-	param "github.com/ksonnet/ksonnet/metadata/params"
 	"github.com/spf13/afero"
 )
 
@@ -40,216 +41,228 @@ var (
 	mockEnvs      = []string{defaultEnvName, mockEnvName, mockEnvName2, mockEnvName3}
 )
 
-func mockEnvironments(t *testing.T, appName string) *manager {
-	return mockEnvironmentsWith(t, appName, mockEnvs)
+func mockEnvironments(t *testing.T, fs afero.Fs, appName string) *manager {
+	return mockEnvironmentsWith(t, fs, appName, mockEnvs)
 }
 
-func mockEnvironmentsWith(t *testing.T, appName string, envNames []string) *manager {
+func mockEnvironmentsWith(t *testing.T, fs afero.Fs, appName string, envNames []string) *manager {
 	specFlag := fmt.Sprintf("file:%s", blankSwagger)
 
 	reg := newMockRegistryManager("incubator")
-	m, err := initManager(appName, appName, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
+	root := filepath.Join("/", appName)
+	m, err := initManager(appName, root, &specFlag, &mockAPIServer, &mockNamespace, reg, fs)
 	if err != nil {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
 
 	for _, env := range envNames {
 		envPath := str.AppendToPath(m.environmentsPath, env)
-		testFS.Mkdir(envPath, defaultFolderPermissions)
-		testDirExists(t, envPath)
+		fs.Mkdir(envPath, defaultFolderPermissions)
+		testDirExists(t, fs, envPath)
 
 		envFilePath := str.AppendToPath(envPath, envFileName)
 		envFileData := m.generateOverrideData()
-		err = afero.WriteFile(testFS, envFilePath, envFileData, defaultFilePermissions)
+		err = afero.WriteFile(fs, envFilePath, envFileData, defaultFilePermissions)
 		if err != nil {
 			t.Fatalf("Could not write file at path: %s", envFilePath)
 		}
-		testFileExists(t, envFilePath)
+		testFileExists(t, fs, envFilePath)
 
 		paramsPath := str.AppendToPath(envPath, paramsFileName)
 		paramsData := m.generateParamsData()
-		err = afero.WriteFile(testFS, paramsPath, paramsData, defaultFilePermissions)
+		err = afero.WriteFile(fs, paramsPath, paramsData, defaultFilePermissions)
 		if err != nil {
 			t.Fatalf("Could not write file at path: %s", paramsPath)
 		}
-		testFileExists(t, paramsPath)
+		testFileExists(t, fs, paramsPath)
 
-		appSpec, err := m.AppSpec()
+		appSpec, err := app.Read(m.appFS, m.rootPath)
 		if err != nil {
 			t.Fatal("Could not retrieve app spec")
 		}
 		appSpec.AddEnvironmentSpec(&app.EnvironmentSpec{
-			Name: env,
-			Path: env,
+			Name:              env,
+			Path:              env,
+			KubernetesVersion: "v1.8.7",
 			Destination: &app.EnvironmentDestinationSpec{
 				Server:    mockAPIServer,
 				Namespace: mockNamespace,
 			},
 		})
-		m.WriteAppSpec(appSpec)
+		err = app.Write(m.appFS, m.rootPath, appSpec)
+		require.NoError(t, err)
 	}
 
 	return m
 }
 
-func testDirExists(t *testing.T, path string) {
-	exists, err := afero.DirExists(testFS, path)
-	if err != nil {
-		t.Fatalf("Expected directory at '%s' to exist, but failed:\n%v", path, err)
-	} else if !exists {
-		t.Fatalf("Expected directory at '%s' to exist, but it does not", path)
-	}
+func testDirExists(t *testing.T, fs afero.Fs, path string) {
+	exists, err := afero.DirExists(fs, path)
+	require.NoError(t, err, "Checking %q failed", path)
+	require.True(t, exists, "Expected directory %q to exist", path)
 }
 
-func testDirNotExists(t *testing.T, path string) {
-	exists, err := afero.DirExists(testFS, path)
-	if err != nil {
-		t.Fatalf("Expected directory at '%s' to be removed, but failed:\n%v", path, err)
-	} else if exists {
-		t.Fatalf("Expected directory at '%s' to be removed, but it exists", path)
-	}
+func testDirNotExists(t *testing.T, fs afero.Fs, path string) {
+	exists, err := afero.DirExists(fs, path)
+	require.NoError(t, err, "Checking %q failed", path)
+	require.False(t, exists, "Expected directory %q to not exist", path)
 }
 
-func testFileExists(t *testing.T, path string) {
-	exists, err := afero.Exists(testFS, path)
-	if err != nil {
-		t.Fatalf("Expected file at '%s' to exist, but failed:\n%v", path, err)
-	} else if !exists {
-		t.Fatalf("Expected file at '%s' to exist, but it does not", path)
-	}
+func testFileExists(t *testing.T, fs afero.Fs, path string) {
+	exists, err := afero.Exists(fs, path)
+	require.NoError(t, err, "Checking %q failed", path)
+	require.True(t, exists, "Expected file %q to exist", path)
 }
 
 func TestDeleteEnvironment(t *testing.T) {
-	appName := "test-delete-envs"
-	m := mockEnvironments(t, appName)
-
-	// Test that both directory and empty parent directory is deleted.
-	expectedPath := str.AppendToPath(m.environmentsPath, mockEnvName3)
-	parentDir := strings.Split(mockEnvName3, "/")[0]
-	expectedParentPath := str.AppendToPath(m.environmentsPath, parentDir)
-	err := m.DeleteEnvironment(mockEnvName3)
-	if err != nil {
-		t.Fatalf("Expected %s to be deleted but got err:\n  %s", mockEnvName3, err)
-	}
-	testDirNotExists(t, expectedPath)
-	testDirNotExists(t, expectedParentPath)
-
-	// Test that only leaf directory is deleted if parent directory is shared
-	expectedPath = str.AppendToPath(m.environmentsPath, mockEnvName2)
-	parentDir = strings.Split(mockEnvName2, "/")[0]
-	expectedParentPath = str.AppendToPath(m.environmentsPath, parentDir)
-	err = m.DeleteEnvironment(mockEnvName2)
-	if err != nil {
-		t.Fatalf("Expected %s to be deleted but got err:\n  %s", mockEnvName3, err)
-	}
-	testDirNotExists(t, expectedPath)
-	testDirExists(t, expectedParentPath)
+	withFs(func(fs afero.Fs) {
+		appName := "test-delete-envs"
+		appMock := &mocks.App{}
+		appMock.On("RemoveEnvironment", "us-east/test").Return(nil)
+		appMock.On("RemoveEnvironment", "us-west/prod").Return(nil)
+
+		m := mockEnvironments(t, fs, appName)
+
+		// Test that both directory and empty parent directory is deleted.
+		expectedPath, err := filepath.Abs(filepath.Join("/", m.rootPath, "environments", mockEnvName3))
+		require.NoError(t, err)
+		parentDir := strings.Split(mockEnvName3, "/")[0]
+		expectedParentPath, err := filepath.Abs(filepath.Join("/", m.rootPath, "environments", parentDir))
+		require.NoError(t, err)
+		err = m.DeleteEnvironment(mockEnvName3)
+		if err != nil {
+			t.Fatalf("Expected %s to be deleted but got err:\n  %s", mockEnvName3, err)
+		}
+		testDirNotExists(t, fs, expectedPath)
+		testDirNotExists(t, fs, expectedParentPath)
+
+		// Test that only leaf directory is deleted if parent directory is shared
+		expectedPath = str.AppendToPath("/", m.environmentsPath, mockEnvName2)
+		parentDir = strings.Split(mockEnvName2, "/")[0]
+		expectedParentPath = str.AppendToPath("/", m.environmentsPath, parentDir)
+		err = m.DeleteEnvironment(mockEnvName2)
+		if err != nil {
+			t.Fatalf("Expected %s to be deleted but got err:\n  %s", mockEnvName3, err)
+		}
+
+		testDirNotExists(t, fs, expectedPath)
+		testDirExists(t, fs, expectedParentPath)
+	})
 }
 
 func TestGetEnvironments(t *testing.T) {
-	m := mockEnvironments(t, "test-get-envs")
+	withFs(func(fs afero.Fs) {
+		appMock := &mocks.App{}
+		appMock.On("Environments").Return(nil, nil)
 
-	envs, err := m.GetEnvironments()
-	if err != nil {
-		t.Fatalf("Expected to successfully get environments but failed:\n  %s", err)
-	}
+		m := mockEnvironments(t, fs, "test-get-envs")
 
-	if len(envs) != 4 {
-		t.Fatalf("Expected to get %d environments, got %d", 4, len(envs))
-	}
+		envs, err := m.GetEnvironments()
+		if err != nil {
+			t.Fatalf("Expected to successfully get environments but failed:\n  %s", err)
+		}
 
-	name := envs[mockEnvName].Name
-	if name != mockEnvName {
-		t.Fatalf("Expected env name to be '%s', got '%s'", mockEnvName, name)
-	}
+		if len(envs) != 4 {
+			t.Fatalf("Expected to get %d environments, got %d", 4, len(envs))
+		}
 
-	server := envs[mockEnvName].Destination.Server
-	if server != mockAPIServer {
-		t.Fatalf("Expected env server to be %s, got %s", mockAPIServer, server)
-	}
+		cur := envs[mockEnvName]
+		name := cur.Name
+		if name != mockEnvName {
+			t.Fatalf("Expected env name to be %q, got %q", mockEnvName, name)
+		}
+
+		server := cur.Destination.Server()
+		if server != mockAPIServer {
+			t.Fatalf("Expected env server to be %q, got %q", mockAPIServer, server)
+		}
+	})
 }
 
 func TestSetEnvironment(t *testing.T) {
-	appName := "test-set-envs"
-	m := mockEnvironments(t, appName)
+	withFs(func(fs afero.Fs) {
+		appName := "test-set-envs"
+		m := mockEnvironments(t, fs, appName)
 
-	setName := "new-env"
+		setName := "new-env"
 
-	// Test updating an environment that doesn't exist
-	err := m.SetEnvironment("notexists", setName)
-	if err == nil {
-		t.Fatal("Expected error when setting an environment that does not exist")
-	}
+		// Test updating an environment that doesn't exist
+		err := m.SetEnvironment("notexists", setName)
+		if err == nil {
+			t.Fatal("Expected error when setting an environment that does not exist")
+		}
 
-	// Test updating an environment to an environment that already exists
-	err = m.SetEnvironment(mockEnvName, mockEnvName2)
-	if err == nil {
-		t.Fatalf("Expected error when setting \"%s\" to \"%s\", because env already exists", mockEnvName, mockEnvName2)
-	}
+		// Test updating an environment to an environment that already exists
+		err = m.SetEnvironment(mockEnvName, mockEnvName2)
+		if err == nil {
+			t.Fatalf("Expected error when setting \"%s\" to \"%s\", because env already exists", mockEnvName, mockEnvName2)
+		}
 
-	// Test changing the name an existing environment.
-	err = m.SetEnvironment(mockEnvName, setName)
-	if err != nil {
-		t.Fatalf("Could not set \"%s\", got:\n  %s", mockEnvName, err)
-	}
+		// Test changing the name an existing environment.
+		err = m.SetEnvironment(mockEnvName, setName)
+		if err != nil {
+			t.Fatalf("Could not set \"%s\", got:\n  %s", mockEnvName, err)
+		}
 
-	// Ensure new env directory is created, and old directory no longer exists.
-	envPath := str.AppendToPath(appName, environmentsDir)
-	expectedPathExists := str.AppendToPath(envPath, setName)
-	expectedPathNotExists := str.AppendToPath(envPath, mockEnvName)
-	testDirExists(t, expectedPathExists)
-	testDirNotExists(t, expectedPathNotExists)
-
-	// BUG: https://github.com/spf13/afero/issues/141
-	// we aren't able to test this until the above is fixed.
-	//
-	// ensure all files are moved
-	//
-	// expectedFiles := []string{
-	// 	envFileName,
-	// 	specFilename,
-	// 	paramsFileName,
-	// }
-	// for _, f := range expectedFiles {
-	// 	expectedFilePath := appendToAbsPath(expectedPathExists, f)
-	// 	testFileExists(t, string(expectedFilePath))
-	// }
-
-	tests := []struct {
-		appName string
-		nameOld string
-		nameNew string
-	}{
-		// Test changing the name of an env 'us-west' to 'us-west/dev'
-		{
-			"test-set-to-child",
-			"us-west",
-			"us-west/dev",
-		},
-		// Test changing the name of an env 'us-west/dev' to 'us-west'
-		{
-			"test-set-to-parent",
-			"us-west/dev",
-			"us-west",
-		},
-	}
+		// Ensure new env directory is created, and old directory no longer exists.
+		envPath := str.AppendToPath(appName, environmentsDir)
+		expectedPathExists := filepath.Join("/", envPath, setName)
+		expectedPathNotExists := filepath.Join("/", envPath, mockEnvName)
+		testDirExists(t, fs, expectedPathExists)
+		testDirNotExists(t, fs, expectedPathNotExists)
+
+		// BUG: https://github.com/spf13/afero/issues/141
+		// we aren't able to test this until the above is fixed.
+		//
+		// ensure all files are moved
+		//
+		// expectedFiles := []string{
+		// 	envFileName,
+		// 	specFilename,
+		// 	paramsFileName,
+		// }
+		// for _, f := range expectedFiles {
+		// 	expectedFilePath := appendToAbsPath(expectedPathExists, f)
+		// 	testFileExists(t, string(expectedFilePath))
+		// }
+
+		tests := []struct {
+			appName string
+			nameOld string
+			nameNew string
+		}{
+			// Test changing the name of an env 'us-west' to 'us-west/dev'
+			{
+				"test-set-to-child",
+				"us-west",
+				"us-west/dev",
+			},
+			// Test changing the name of an env 'us-west/dev' to 'us-west'
+			{
+				"test-set-to-parent",
+				"us-west/dev",
+				"us-west",
+			},
+		}
 
-	for _, v := range tests {
-		m = mockEnvironmentsWith(t, v.appName, []string{v.nameOld})
-		err = m.SetEnvironment(v.nameOld, v.nameNew)
-		if err != nil {
-			t.Fatalf("Could not set '%s', got:\n  %s", v.nameOld, err)
+		for _, v := range tests {
+			m = mockEnvironmentsWith(t, fs, v.appName, []string{v.nameOld})
+			err = m.SetEnvironment(v.nameOld, v.nameNew)
+			if err != nil {
+				t.Fatalf("Could not set '%s', got:\n  %s", v.nameOld, err)
+			}
+			// Ensure new env directory is created
+			expectedPath := filepath.Join("/", v.appName, environmentsDir, v.nameNew)
+			testDirExists(t, fs, expectedPath)
 		}
-		// Ensure new env directory is created
-		expectedPath := str.AppendToPath(v.appName, environmentsDir, v.nameNew)
-		testDirExists(t, expectedPath)
-	}
+	})
 }
 
 func TestGenerateOverrideData(t *testing.T) {
-	m := mockEnvironments(t, "test-gen-override-data")
+	withFs(func(fs afero.Fs) {
+		m := mockEnvironments(t, fs, "test-gen-override-data")
 
-	expected := `local base = import "base.libsonnet";
+		expected := `local base = import "base.libsonnet";
 local k = import "k.libsonnet";
 
 base + {
@@ -257,17 +270,19 @@ base + {
   //   "nginx-deployment"+: k.deployment.mixin.metadata.labels({foo: "bar"})
 }
 `
-	result := m.generateOverrideData()
+		result := m.generateOverrideData()
 
-	if string(result) != expected {
-		t.Fatalf("Expected to generate override file with data:\n%s\n,got:\n%s", expected, result)
-	}
+		if string(result) != expected {
+			t.Fatalf("Expected to generate override file with data:\n%s\n,got:\n%s", expected, result)
+		}
+	})
 }
 
 func TestGenerateParamsData(t *testing.T) {
-	m := mockEnvironments(t, "test-gen-params-data")
+	withFs(func(fs afero.Fs) {
+		m := mockEnvironments(t, fs, "test-gen-params-data")
 
-	expected := `local params = import "../../components/params.libsonnet";
+		expected := `local params = import "../../components/params.libsonnet";
 params + {
   components +: {
     // Insert component parameter overrides here. Ex:
@@ -278,64 +293,10 @@ params + {
   },
 }
 `
-	result := string(m.generateParamsData())
-
-	if result != expected {
-		t.Fatalf("Expected to generate params file with data:\n%s\n, got:\n%s", expected, result)
-	}
-}
-
-func TestMergeParamMaps(t *testing.T) {
-	tests := []struct {
-		base      map[string]param.Params
-		overrides map[string]param.Params
-		expected  map[string]param.Params
-	}{
-		{
-			map[string]param.Params{
-				"bar": param.Params{"replicas": "5"},
-			},
-			map[string]param.Params{
-				"foo": param.Params{"name": `"foo"`, "replicas": "1"},
-			},
-			map[string]param.Params{
-				"bar": param.Params{"replicas": "5"},
-				"foo": param.Params{"name": `"foo"`, "replicas": "1"},
-			},
-		},
-		{
-			map[string]param.Params{
-				"bar": param.Params{"replicas": "5"},
-			},
-			map[string]param.Params{
-				"bar": param.Params{"name": `"foo"`},
-			},
-			map[string]param.Params{
-				"bar": param.Params{"name": `"foo"`, "replicas": "5"},
-			},
-		},
-		{
-			map[string]param.Params{
-				"bar": param.Params{"name": `"bar"`, "replicas": "5"},
-				"foo": param.Params{"name": `"foo"`, "replicas": "4"},
-				"baz": param.Params{"name": `"baz"`, "replicas": "3"},
-			},
-			map[string]param.Params{
-				"foo": param.Params{"replicas": "1"},
-				"baz": param.Params{"name": `"foobaz"`},
-			},
-			map[string]param.Params{
-				"bar": param.Params{"name": `"bar"`, "replicas": "5"},
-				"foo": param.Params{"name": `"foo"`, "replicas": "1"},
-				"baz": param.Params{"name": `"foobaz"`, "replicas": "3"},
-			},
-		},
-	}
+		result := string(m.generateParamsData())
 
-	for _, s := range tests {
-		result := mergeParamMaps(s.base, s.overrides)
-		if !reflect.DeepEqual(s.expected, result) {
-			t.Errorf("Wrong merge\n  expected:\n%v\n  got:\n%v", s.expected, result)
+		if result != expected {
+			t.Fatalf("Expected to generate params file with data:\n%s\n, got:\n%s", expected, result)
 		}
-	}
+	})
 }
diff --git a/metadata/interface.go b/metadata/interface.go
index 1b4eb4c1024f481b880a2570a9ce591ed216010d..e962c7b7283ea8a6102282559c1b41e7f1aa0a76 100644
--- a/metadata/interface.go
+++ b/metadata/interface.go
@@ -17,9 +17,8 @@ package metadata
 
 import (
 	"os"
-	"regexp"
-	"strings"
 
+	"github.com/ksonnet/ksonnet/env"
 	"github.com/ksonnet/ksonnet/metadata/app"
 	param "github.com/ksonnet/ksonnet/metadata/params"
 	"github.com/ksonnet/ksonnet/metadata/parts"
@@ -36,6 +35,8 @@ var defaultFilePermissions = os.FileMode(0644)
 // things like: create and delete environments; search for prototypes; vendor
 // libraries; and other non-core-application tasks.
 type Manager interface {
+	// App returns the object for the application.
+	App() (app.App, error)
 	Root() string
 	LibPaths() (envPath, vendorPath string)
 	EnvPaths(env string) (libPath, mainPath, paramsPath string, err error)
@@ -54,19 +55,16 @@ type Manager interface {
 	// mapping of parameters of the form:
 	// componentName => {param key => param val}
 	// i.e.: "nginx" => {"replicas" => 1, "name": "nginx"}
-	GetEnvironmentParams(name string) (map[string]param.Params, error)
+	GetEnvironmentParams(name, nsName string) (map[string]param.Params, error)
 	SetEnvironmentParams(env, component string, params param.Params) error
 
 	// Environment API.
 	CreateEnvironment(name, uri, namespace, spec string) error
 	DeleteEnvironment(name string) error
-	GetEnvironments() (app.EnvironmentSpecs, error)
-	GetEnvironment(name string) (*app.EnvironmentSpec, error)
+	GetEnvironments() (map[string]env.Env, error)
+	GetEnvironment(name string) (*env.Env, error)
 	SetEnvironment(name, desiredName string) error
-
-	// Spec API.
-	AppSpec() (*app.Spec, error)
-	WriteAppSpec(*app.Spec) error
+	GetDestination(envName string) (env.Destination, error)
 
 	// Dependency/registry API.
 	AddRegistry(name, protocol, uri, version string) (*registry.Spec, error)
@@ -105,23 +103,6 @@ func Init(name, rootPath string, k8sSpecFlag, serverURI, namespace *string) (Man
 	return initManager(name, rootPath, k8sSpecFlag, serverURI, namespace, gh, appFS)
 }
 
-// isValidName returns true if a name (e.g., for an environment) is valid.
-// Broadly, this means it does not contain punctuation, whitespace, leading or
-// trailing slashes.
-func isValidName(name string) bool {
-	// No unicode whitespace is allowed. `Fields` doesn't handle trailing or
-	// leading whitespace.
-	fields := strings.Fields(name)
-	if len(fields) > 1 || len(strings.TrimSpace(name)) != len(name) {
-		return false
-	}
-
-	hasPunctuation := regexp.MustCompile(`[\\,;':!()?"{}\[\]*&%@$]+`).MatchString
-	hasTrailingSlashes := regexp.MustCompile(`/+$`).MatchString
-	hasLeadingSlashes := regexp.MustCompile(`^/+`).MatchString
-	return len(name) != 0 && !hasPunctuation(name) && !hasTrailingSlashes(name) && !hasLeadingSlashes(name)
-}
-
 func init() {
 	appFS = afero.NewOsFs()
 }
diff --git a/metadata/lib/clusterspec_test.go b/metadata/lib/clusterspec_test.go
index 5c095cbf0c6aa33cfd41f330a381290ca630abc3..a0307acb402b1aa1acbc491dd98f65d3d315dd91 100644
--- a/metadata/lib/clusterspec_test.go
+++ b/metadata/lib/clusterspec_test.go
@@ -16,8 +16,11 @@
 package lib
 
 import (
+	"os"
 	"path/filepath"
 	"testing"
+
+	"github.com/spf13/afero"
 )
 
 type parseSuccess struct {
@@ -25,13 +28,16 @@ type parseSuccess struct {
 	target ClusterSpec
 }
 
-var successTests = []parseSuccess{
-	{"version:v1.7.1", &clusterSpecVersion{"v1.7.1"}},
-	{"file:swagger.json", &clusterSpecFile{"swagger.json", testFS}},
-	{"url:file:///some_file", &clusterSpecLive{"file:///some_file"}},
-}
-
 func TestClusterSpecParsingSuccess(t *testing.T) {
+	testFS := afero.NewMemMapFs()
+	afero.WriteFile(testFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
+
+	var successTests = []parseSuccess{
+		{"version:v1.7.1", &clusterSpecVersion{"v1.7.1"}},
+		{"file:swagger.json", &clusterSpecFile{"swagger.json", testFS}},
+		{"url:file:///some_file", &clusterSpecLive{"file:///some_file"}},
+	}
+
 	for _, test := range successTests {
 		parsed, err := ParseClusterSpec(test.input, testFS)
 		if err != nil {
@@ -80,7 +86,10 @@ var failureTests = []parseFailure{
 }
 
 func TestClusterSpecParsingFailure(t *testing.T) {
+
 	for _, test := range failureTests {
+		testFS := afero.NewMemMapFs()
+		afero.WriteFile(testFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
 		_, err := ParseClusterSpec(test.input, testFS)
 		if err == nil {
 			t.Errorf("Cluster spec parse for '%s' should have failed, but succeeded", test.input)
diff --git a/metadata/lib/lib.go b/metadata/lib/lib.go
index 8be7d908ecda2e7d1c9f710d5bfd840217829776..4feb6a7973e586f410f711d0efab888f8d56da5b 100644
--- a/metadata/lib/lib.go
+++ b/metadata/lib/lib.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"os"
 	"path"
+	"path/filepath"
 
 	str "github.com/ksonnet/ksonnet/strings"
 	log "github.com/sirupsen/logrus"
@@ -70,7 +71,7 @@ func NewManager(k8sSpecFlag string, fs afero.Fs, libPath string) (*Manager, erro
 // directory of a ksonnet project. The swagger and ksonnet-lib files are
 // unique to each Kubernetes API version. If the files already exist for a
 // specific Kubernetes API version, they won't be re-generated here.
-func (m *Manager) GenerateLibData() error {
+func (m *Manager) GenerateLibData(useVersionPath bool) error {
 	if m.spec == nil {
 		return fmt.Errorf("Uninitialized ClusterSpec")
 	}
@@ -85,8 +86,13 @@ func (m *Manager) GenerateLibData() error {
 		return err
 	}
 
-	versionPath := str.AppendToPath(m.libPath, m.K8sVersion)
-	ok, err := afero.DirExists(m.fs, string(versionPath))
+	genPath := m.libPath
+
+	if useVersionPath {
+		genPath = filepath.Join(m.libPath, m.K8sVersion)
+	}
+
+	ok, err := afero.DirExists(m.fs, genPath)
 	if err != nil {
 		return err
 	}
@@ -95,7 +101,7 @@ func (m *Manager) GenerateLibData() error {
 		return nil
 	}
 
-	err = m.fs.MkdirAll(string(versionPath), os.FileMode(0755))
+	err = m.fs.MkdirAll(genPath, os.FileMode(0755))
 	if err != nil {
 		return err
 	}
@@ -106,22 +112,22 @@ func (m *Manager) GenerateLibData() error {
 	}{
 		{
 			// schema file
-			str.AppendToPath(versionPath, schemaFilename),
+			filepath.Join(genPath, schemaFilename),
 			kl.Swagger,
 		},
 		{
 			// k8s file
-			str.AppendToPath(versionPath, k8sLibFilename),
+			filepath.Join(genPath, k8sLibFilename),
 			kl.K8s,
 		},
 		{
 			// extensions file
-			str.AppendToPath(versionPath, ExtensionsLibFilename),
+			filepath.Join(genPath, ExtensionsLibFilename),
 			kl.K,
 		},
 	}
 
-	log.Infof("Generating ksonnet-lib data at path '%s'", versionPath)
+	log.Infof("Generating ksonnet-lib data at path '%s'", genPath)
 
 	for _, a := range files {
 		fileName := path.Base(string(a.path))
@@ -136,7 +142,7 @@ func (m *Manager) GenerateLibData() error {
 
 // GetLibPath returns the absolute path pointing to the directory with the
 // metadata files for the provided k8sVersion.
-func (m *Manager) GetLibPath() (string, error) {
+func (m *Manager) GetLibPath(useVersionPath bool) (string, error) {
 	path := str.AppendToPath(m.libPath, m.K8sVersion)
 	ok, err := afero.DirExists(m.fs, string(path))
 	if err != nil {
@@ -146,7 +152,7 @@ func (m *Manager) GetLibPath() (string, error) {
 		log.Debugf("Expected lib directory '%s' but was not found", m.K8sVersion)
 
 		// create the directory
-		if err := m.GenerateLibData(); err != nil {
+		if err = m.GenerateLibData(useVersionPath); err != nil {
 			return "", err
 		}
 
diff --git a/metadata/lib/lib_test.go b/metadata/lib/lib_test.go
index 8e070e975f97cbe61591ebf76d2034416aaf3887..f48384f1e592e207de0dcdbf437907fd078d223c 100644
--- a/metadata/lib/lib_test.go
+++ b/metadata/lib/lib_test.go
@@ -16,13 +16,12 @@ package lib
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
+	"path/filepath"
 	"testing"
 
 	"github.com/spf13/afero"
-
-	str "github.com/ksonnet/ksonnet/strings"
+	"github.com/stretchr/testify/require"
 )
 
 const (
@@ -40,51 +39,56 @@ const (
 }`
 )
 
-var testFS = afero.NewMemMapFs()
-
-func init() {
-	afero.WriteFile(testFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
-}
-
 func TestGenerateLibData(t *testing.T) {
-	specFlag := fmt.Sprintf("file:%s", blankSwagger)
-	libPath := "lib"
-
-	libManager, err := NewManager(specFlag, testFS, libPath)
-	if err != nil {
-		t.Fatal("Failed to initialize lib.Manager")
+	cases := []struct {
+		name           string
+		useVersionPath bool
+		basePath       string
+	}{
+		{
+			name:           "use version path",
+			useVersionPath: true,
+			basePath:       "v1.7.0",
+		},
+		{
+			name: "don't use version path",
+		},
 	}
 
-	err = libManager.GenerateLibData()
-	if err != nil {
-		t.Fatal("Failed to generate lib data")
-	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			testFS := afero.NewMemMapFs()
+			afero.WriteFile(testFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
 
-	// Verify contents of lib.
-	versionPath := str.AppendToPath(libPath, "v1.7.0")
+			specFlag := fmt.Sprintf("file:%s", blankSwagger)
+			libPath := "lib"
 
-	schemaPath := str.AppendToPath(versionPath, schemaFilename)
-	bytes, err := afero.ReadFile(testFS, string(schemaPath))
-	if err != nil {
-		t.Fatalf("Failed to read swagger file at '%s':\n%v", schemaPath, err)
-	}
+			libManager, err := NewManager(specFlag, testFS, libPath)
+			if err != nil {
+				t.Fatal("Failed to initialize lib.Manager")
+			}
 
-	if actualSwagger := string(bytes); actualSwagger != blankSwaggerData {
-		t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", schemaPath, blankSwaggerData, actualSwagger)
-	}
+			err = libManager.GenerateLibData(tc.useVersionPath)
+			if err != nil {
+				t.Fatal("Failed to generate lib data")
+			}
 
-	k8sLibPath := str.AppendToPath(versionPath, k8sLibFilename)
-	k8sLibBytes, err := afero.ReadFile(testFS, string(k8sLibPath))
-	if err != nil {
-		t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", k8sLibPath, err)
-	}
+			// Verify contents of lib.
+			genPath := filepath.Join(libPath, tc.basePath)
+			schemaPath := filepath.Join(genPath, "swagger.json")
+			extPath := filepath.Join(genPath, "k.libsonnet")
+			k8sPath := filepath.Join(genPath, "k8s.libsonnet")
 
-	blankK8sLib, err := ioutil.ReadFile("testdata/k8s.libsonnet")
-	if err != nil {
-		t.Fatalf("Failed to read testdata/k8s.libsonnet: %#v", err)
+			checkExists(t, testFS, schemaPath)
+			checkExists(t, testFS, extPath)
+			checkExists(t, testFS, k8sPath)
+		})
 	}
 
-	if actualK8sLib := string(k8sLibBytes); actualK8sLib != string(blankK8sLib) {
-		t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", k8sLibPath, blankK8sLib, actualK8sLib)
-	}
+}
+
+func checkExists(t *testing.T, fs afero.Fs, path string) {
+	exists, err := afero.Exists(fs, path)
+	require.NoError(t, err)
+	require.True(t, exists, "%q did not exist", path)
 }
diff --git a/metadata/lib/testdata/k8s.libsonnet b/metadata/lib/testdata/k8s.libsonnet
deleted file mode 100644
index 6d4307b8ddfd27956ccc7ca36287d5116e11e542..0000000000000000000000000000000000000000
--- a/metadata/lib/testdata/k8s.libsonnet
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "__ksonnet": {
-    checksum: "f942f2f2b70d842504ffa21b502ad044be92110af159342a352bf1ed4c6221e2",
-    kubernetesVersion: "1.7.0",
-  },
-  local hidden = {
-  },
-}
\ No newline at end of file
diff --git a/metadata/manager.go b/metadata/manager.go
index 9bae3038ca922617da60ab886a8f910aef7a10fe..6ad56644ae1e4035c171f5028cb0f21f76d9f9c8 100644
--- a/metadata/manager.go
+++ b/metadata/manager.go
@@ -22,9 +22,11 @@ import (
 	"path/filepath"
 
 	"github.com/ksonnet/ksonnet/component"
+	"github.com/ksonnet/ksonnet/env"
 	"github.com/ksonnet/ksonnet/metadata/app"
 	"github.com/ksonnet/ksonnet/metadata/registry"
 	str "github.com/ksonnet/ksonnet/strings"
+	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
 )
@@ -88,7 +90,21 @@ func findManager(p string, appFS afero.Fs) (*manager, error) {
 			return nil, err
 		}
 		if exists {
-			return newManager(currBase, appFS)
+			m, err := newManager(currBase, appFS)
+			if err != nil {
+				return nil, err
+			}
+
+			app, err := m.App()
+			if err != nil {
+				return nil, err
+			}
+
+			if err = app.Init(); err != nil {
+				return nil, errors.Wrap(err, "initialize app schema")
+			}
+
+			return m, nil
 		}
 
 		lastBase = currBase
@@ -100,9 +116,10 @@ func findManager(p string, appFS afero.Fs) (*manager, error) {
 }
 
 func initManager(name, rootPath string, k8sSpecFlag, serverURI, namespace *string, incubatorReg registry.Manager, appFS afero.Fs) (*manager, error) {
+	log.Info("init manager")
 	m, err := newManager(rootPath, appFS)
 	if err != nil {
-		return nil, err
+		return nil, errors.Wrap(err, "create manager")
 	}
 
 	// Retrieve `registry.yaml`.
@@ -139,7 +156,7 @@ func initManager(name, rootPath string, k8sSpecFlag, serverURI, namespace *strin
 	}
 
 	// Write out `incubator` registry spec.
-	registryPath := string(m.registryPath(incubatorReg))
+	registryPath := m.registryPath(incubatorReg)
 	err = afero.WriteFile(m.appFS, registryPath, registryYAMLData, defaultFilePermissions)
 	if err != nil {
 		return nil, errorOnCreateFailure(name, err)
@@ -155,7 +172,7 @@ func newManager(rootPath string, appFS afero.Fs) (*manager, error) {
 	}
 	userRootPath := str.AppendToPath(usr.HomeDir, userKsonnetRootDir)
 
-	return &manager{
+	m := &manager{
 		appFS: appFS,
 
 		// Application paths.
@@ -174,17 +191,32 @@ func newManager(rootPath string, appFS afero.Fs) (*manager, error) {
 		// User-level paths.
 		userKsonnetRootPath: userRootPath,
 		pkgSrcCachePath:     str.AppendToPath(userRootPath, pkgSrcCacheDir),
-	}, nil
+	}
+
+	return m, nil
 }
 
 func (m *manager) Root() string {
 	return m.rootPath
 }
 
+func (m *manager) App() (app.App, error) {
+	return app.Load(m.appFS, m.rootPath)
+}
+
 func (m *manager) LibPaths() (envPath, vendorPath string) {
 	return m.environmentsPath, m.vendorPath
 }
 
+func (m *manager) GetDestination(envName string) (env.Destination, error) {
+	appEnv, err := m.GetEnvironment(envName)
+	if err != nil {
+		return env.Destination{}, err
+	}
+
+	return appEnv.Destination, nil
+}
+
 func (m *manager) createUserDirTree() error {
 	dirPaths := []string{
 		m.userKsonnetRootPath,
@@ -203,7 +235,7 @@ func (m *manager) createUserDirTree() error {
 func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte, gh registry.Manager) error {
 	exists, err := afero.DirExists(m.appFS, m.rootPath)
 	if err != nil {
-		return fmt.Errorf("Could not check existance of directory '%s':\n%v", m.rootPath, err)
+		return fmt.Errorf("Could not check existence of directory '%s':\n%v", m.rootPath, err)
 	} else if exists {
 		return fmt.Errorf("Could not create app; directory '%s' already exists", m.rootPath)
 	}
@@ -221,7 +253,7 @@ func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte,
 
 	for _, p := range dirPaths {
 		log.Debugf("Creating directory '%s'", p)
-		if err := m.appFS.MkdirAll(string(p), defaultFolderPermissions); err != nil {
+		if err := m.appFS.MkdirAll(p, defaultFolderPermissions); err != nil {
 			return errorOnCreateFailure(name, err)
 		}
 	}
diff --git a/metadata/manager_test.go b/metadata/manager_test.go
index aeb1e65068d473956a752b1df40c9ab27970c779..b566a18d10f0241fe8e439bf5418d8c4e280336b 100644
--- a/metadata/manager_test.go
+++ b/metadata/manager_test.go
@@ -21,6 +21,7 @@ import (
 	"path"
 	"testing"
 
+	"github.com/ksonnet/ksonnet/metadata/app"
 	str "github.com/ksonnet/ksonnet/strings"
 	"github.com/spf13/afero"
 )
@@ -48,191 +49,210 @@ const (
 `
 )
 
-var testFS = afero.NewMemMapFs()
+func withFs(fn func(afero.Fs)) {
+	ogAppLibUpdater := app.LibUpdater
+	app.LibUpdater = app.StubUpdateLibData
 
-func init() {
-	afero.WriteFile(testFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
+	defer func() {
+		app.LibUpdater = ogAppLibUpdater
+	}()
+
+	fs := afero.NewMemMapFs()
+	afero.WriteFile(fs, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
+	fn(fs)
 }
 
 func TestInitSuccess(t *testing.T) {
-	specFlag := fmt.Sprintf("file:%s", blankSwagger)
-
-	appPath := "/fromEmptySwagger"
-	reg := newMockRegistryManager("incubator")
-	_, err := initManager("fromEmptySwagger", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
-	if err != nil {
-		t.Fatalf("Failed to init cluster spec: %v", err)
-	}
-
-	// Verify path locations.
-	defaultEnvDir := str.AppendToPath(environmentsDir, defaultEnvName)
-	paths := []string{
-		ksonnetDir,
-		libDir,
-		componentsDir,
-		environmentsDir,
-		vendorDir,
-		defaultEnvDir,
-	}
-
-	for _, p := range paths {
-		path := str.AppendToPath(appPath, p)
-		exists, err := afero.DirExists(testFS, path)
+	withFs(func(fs afero.Fs) {
+		specFlag := fmt.Sprintf("file:%s", blankSwagger)
+
+		appPath := "/fromEmptySwagger"
+		reg := newMockRegistryManager("incubator")
+		_, err := initManager("fromEmptySwagger", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, fs)
 		if err != nil {
-			t.Fatalf("Expected to create directory '%s', but failed:\n%v", p, err)
-		} else if !exists {
-			t.Fatalf("Expected to create directory '%s', but failed", path)
+			t.Fatalf("Failed to init cluster spec: %v", err)
 		}
-	}
 
-	paths = []string{
-		pkgSrcCacheDir,
-	}
+		// Verify path locations.
+		defaultEnvDir := str.AppendToPath(environmentsDir, defaultEnvName)
+		paths := []string{
+			ksonnetDir,
+			libDir,
+			componentsDir,
+			environmentsDir,
+			vendorDir,
+			defaultEnvDir,
+		}
+
+		for _, p := range paths {
+			path := str.AppendToPath(appPath, p)
+			exists, err := afero.DirExists(fs, path)
+			if err != nil {
+				t.Fatalf("Expected to create directory '%s', but failed:\n%v", p, err)
+			} else if !exists {
+				t.Fatalf("Expected to create directory '%s', but failed", path)
+			}
+		}
 
-	usr, err := user.Current()
-	if err != nil {
-		t.Fatalf("Could not get user information:\n%v", err)
-	}
-	userRootPath := str.AppendToPath(usr.HomeDir, userKsonnetRootDir)
+		paths = []string{
+			pkgSrcCacheDir,
+		}
 
-	for _, p := range paths {
-		path := str.AppendToPath(userRootPath, p)
-		exists, err := afero.DirExists(testFS, path)
+		usr, err := user.Current()
 		if err != nil {
-			t.Fatalf("Expected to create directory '%s', but failed:\n%v", p, err)
-		} else if !exists {
-			t.Fatalf("Expected to create directory '%s', but failed", path)
-		}
-	}
-
-	// Verify contents of metadata.
-	envPath := str.AppendToPath(appPath, environmentsDir)
-
-	componentParamsPath := str.AppendToPath(appPath, componentsDir, componentParamsFile)
-	componentParamsBytes, err := afero.ReadFile(testFS, componentParamsPath)
-	if err != nil {
-		t.Fatalf("Failed to read params.libsonnet file at '%s':\n%v", componentParamsPath, err)
-	} else if len(componentParamsBytes) == 0 {
-		t.Fatalf("Expected params.libsonnet at '%s' to be non-empty", componentParamsPath)
-	}
-
-	baseLibsonnetPath := str.AppendToPath(envPath, baseLibsonnetFile)
-	baseLibsonnetBytes, err := afero.ReadFile(testFS, baseLibsonnetPath)
-	if err != nil {
-		t.Fatalf("Failed to read base.libsonnet file at '%s':\n%v", baseLibsonnetPath, err)
-	} else if len(baseLibsonnetBytes) == 0 {
-		t.Fatalf("Expected base.libsonnet at '%s' to be non-empty", baseLibsonnetPath)
-	}
-
-	appYAMLPath := str.AppendToPath(appPath, appYAMLFile)
-	appYAMLBytes, err := afero.ReadFile(testFS, appYAMLPath)
-	if err != nil {
-		t.Fatalf("Failed to read app.yaml file at '%s':\n%v", appYAMLPath, err)
-	} else if len(appYAMLBytes) == 0 {
-		t.Fatalf("Expected app.yaml at '%s' to be non-empty", appYAMLPath)
-	}
-
-	registryYAMLPath := str.AppendToPath(appPath, registriesDir, "incubator", "master.yaml")
-	registryYAMLBytes, err := afero.ReadFile(testFS, registryYAMLPath)
-	if err != nil {
-		t.Fatalf("Failed to read registry.yaml file at '%s':\n%v", registryYAMLPath, err)
-	} else if len(registryYAMLBytes) == 0 {
-		t.Fatalf("Expected registry.yaml at '%s' to be non-empty", registryYAMLPath)
-	}
+			t.Fatalf("Could not get user information:\n%v", err)
+		}
+		userRootPath := str.AppendToPath(usr.HomeDir, userKsonnetRootDir)
+
+		for _, p := range paths {
+			path := str.AppendToPath(userRootPath, p)
+			exists, err := afero.DirExists(fs, path)
+			if err != nil {
+				t.Fatalf("Expected to create directory '%s', but failed:\n%v", p, err)
+			} else if !exists {
+				t.Fatalf("Expected to create directory '%s', but failed", path)
+			}
+		}
+
+		// Verify contents of metadata.
+		envPath := str.AppendToPath(appPath, environmentsDir)
+
+		componentParamsPath := str.AppendToPath(appPath, componentsDir, componentParamsFile)
+		componentParamsBytes, err := afero.ReadFile(fs, componentParamsPath)
+		if err != nil {
+			t.Fatalf("Failed to read params.libsonnet file at '%s':\n%v", componentParamsPath, err)
+		} else if len(componentParamsBytes) == 0 {
+			t.Fatalf("Expected params.libsonnet at '%s' to be non-empty", componentParamsPath)
+		}
+
+		baseLibsonnetPath := str.AppendToPath(envPath, baseLibsonnetFile)
+		baseLibsonnetBytes, err := afero.ReadFile(fs, baseLibsonnetPath)
+		if err != nil {
+			t.Fatalf("Failed to read base.libsonnet file at '%s':\n%v", baseLibsonnetPath, err)
+		} else if len(baseLibsonnetBytes) == 0 {
+			t.Fatalf("Expected base.libsonnet at '%s' to be non-empty", baseLibsonnetPath)
+		}
+
+		appYAMLPath := str.AppendToPath(appPath, appYAMLFile)
+		appYAMLBytes, err := afero.ReadFile(fs, appYAMLPath)
+		if err != nil {
+			t.Fatalf("Failed to read app.yaml file at '%s':\n%v", appYAMLPath, err)
+		} else if len(appYAMLBytes) == 0 {
+			t.Fatalf("Expected app.yaml at '%s' to be non-empty", appYAMLPath)
+		}
+
+		registryYAMLPath := str.AppendToPath(appPath, registriesDir, "incubator", "master.yaml")
+		registryYAMLBytes, err := afero.ReadFile(fs, registryYAMLPath)
+		if err != nil {
+			t.Fatalf("Failed to read registry.yaml file at '%s':\n%v", registryYAMLPath, err)
+		} else if len(registryYAMLBytes) == 0 {
+			t.Fatalf("Expected registry.yaml at '%s' to be non-empty", registryYAMLPath)
+		}
+	})
 }
 
 func TestFindSuccess(t *testing.T) {
-	findSuccess := func(t *testing.T, appDir, currDir string) {
-		m, err := findManager(currDir, testFS)
-		if err != nil {
-			t.Fatalf("Failed to find manager at path '%s':\n%v", currDir, err)
-		} else if m.rootPath != appDir {
-			t.Fatalf("Found manager at incorrect path '%s', expected '%s'", m.rootPath, appDir)
+	withFs(func(fs afero.Fs) {
+		findSuccess := func(t *testing.T, appDir, currDir string) {
+			m, err := findManager(currDir, fs)
+			if err != nil {
+				t.Fatalf("Failed to find manager at path '%s':\n%v", currDir, err)
+			} else if m.rootPath != appDir {
+				t.Fatalf("Found manager at incorrect path '%s', expected '%s'", m.rootPath, appDir)
+			}
 		}
-	}
 
-	specFlag := fmt.Sprintf("file:%s", blankSwagger)
+		specFlag := fmt.Sprintf("file:%s", blankSwagger)
 
-	appPath := "/findSuccess"
-	reg := newMockRegistryManager("incubator")
-	_, err := initManager("findSuccess", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
-	if err != nil {
-		t.Fatalf("Failed to init cluster spec: %v", err)
-	}
+		appPath := "/findSuccess"
+		reg := newMockRegistryManager("incubator")
+		_, err := initManager("findSuccess", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, fs)
+		if err != nil {
+			t.Fatalf("Failed to init cluster spec: %v", err)
+		}
 
-	findSuccess(t, appPath, appPath)
+		findSuccess(t, appPath, appPath)
 
-	components := str.AppendToPath(appPath, componentsDir)
-	findSuccess(t, appPath, components)
+		components := str.AppendToPath(appPath, componentsDir)
+		findSuccess(t, appPath, components)
 
-	// Create empty app file.
-	appFile := str.AppendToPath(components, "app.jsonnet")
-	f, err := testFS.OpenFile(appFile, os.O_RDONLY|os.O_CREATE, 0777)
-	if err != nil {
-		t.Fatalf("Failed to touch app file '%s'\n%v", appFile, err)
-	}
-	f.Close()
+		// Create empty app file.
+		appFile := str.AppendToPath(components, "app.jsonnet")
+		f, err := fs.OpenFile(appFile, os.O_RDONLY|os.O_CREATE, 0777)
+		if err != nil {
+			t.Fatalf("Failed to touch app file '%s'\n%v", appFile, err)
+		}
+		f.Close()
 
-	findSuccess(t, appPath, appFile)
+		findSuccess(t, appPath, appFile)
+	})
 }
 
 func TestLibPaths(t *testing.T) {
-	appName := "test-lib-paths"
-	expectedVendorPath := path.Join(appName, vendorDir)
-	expectedEnvPath := path.Join(appName, environmentsDir)
-	m := mockEnvironments(t, appName)
-
-	envPath, vendorPath := m.LibPaths()
-	if envPath != expectedEnvPath {
-		t.Fatalf("Expected env path to be:\n  '%s'\n, got:\n  '%s'", expectedEnvPath, envPath)
-	}
-	if vendorPath != expectedVendorPath {
-		t.Fatalf("Expected vendor lib path to be:\n  '%s'\n, got:\n  '%s'", expectedVendorPath, vendorPath)
-	}
+	withFs(func(fs afero.Fs) {
+		appName := "test-lib-paths"
+		expectedVendorPath := path.Join("/", appName, vendorDir)
+		expectedEnvPath := path.Join("/", appName, environmentsDir)
+		m := mockEnvironments(t, fs, appName)
+
+		envPath, vendorPath := m.LibPaths()
+		if envPath != expectedEnvPath {
+			t.Fatalf("Expected env path to be:\n  '%s'\n, got:\n  '%s'", expectedEnvPath, envPath)
+		}
+		if vendorPath != expectedVendorPath {
+			t.Fatalf("Expected vendor lib path to be:\n  '%s'\n, got:\n  '%s'", expectedVendorPath, vendorPath)
+		}
+	})
 }
 
 func TestMakeEnvPaths(t *testing.T) {
-	appName := "test-env-paths"
-	expectedMainPath := path.Join(appName, environmentsDir, mockEnvName, envFileName)
-	expectedParamsPath := path.Join(appName, environmentsDir, mockEnvName, paramsFileName)
-	m := mockEnvironments(t, appName)
-
-	mainPath, paramsPath := m.makeEnvPaths(mockEnvName)
-
-	if mainPath != expectedMainPath {
-		t.Fatalf("Expected environment main path to be:\n  '%s'\n, got:\n  '%s'", expectedMainPath, mainPath)
-	}
-	if paramsPath != expectedParamsPath {
-		t.Fatalf("Expected environment params path to be:\n  '%s'\n, got:\n  '%s'", expectedParamsPath, paramsPath)
-	}
+	withFs(func(fs afero.Fs) {
+		appName := "test-env-paths"
+		expectedMainPath := path.Join("/", appName, environmentsDir, mockEnvName, envFileName)
+		expectedParamsPath := path.Join("/", appName, environmentsDir, mockEnvName, paramsFileName)
+		m := mockEnvironments(t, fs, appName)
+
+		mainPath, paramsPath := m.makeEnvPaths(mockEnvName)
+
+		if mainPath != expectedMainPath {
+			t.Fatalf("Expected environment main path to be:\n  '%s'\n, got:\n  '%s'", expectedMainPath, mainPath)
+		}
+		if paramsPath != expectedParamsPath {
+			t.Fatalf("Expected environment params path to be:\n  '%s'\n, got:\n  '%s'", expectedParamsPath, paramsPath)
+		}
+	})
 }
 
 func TestFindFailure(t *testing.T) {
-	findFailure := func(t *testing.T, currDir string) {
-		_, err := findManager(currDir, testFS)
-		if err == nil {
-			t.Fatalf("Expected to fail to find ksonnet app in '%s', but succeeded", currDir)
+	withFs(func(fs afero.Fs) {
+		findFailure := func(t *testing.T, currDir string) {
+			_, err := findManager(currDir, fs)
+			if err == nil {
+				t.Fatalf("Expected to fail to find ksonnet app in '%s', but succeeded", currDir)
+			}
 		}
-	}
 
-	findFailure(t, "/")
-	findFailure(t, "/fakePath")
-	findFailure(t, "")
+		findFailure(t, "/")
+		findFailure(t, "/fakePath")
+		findFailure(t, "")
+	})
 }
 
 func TestDoubleNewFailure(t *testing.T) {
-	specFlag := fmt.Sprintf("file:%s", blankSwagger)
-
-	appPath := "/doubleNew"
-	reg := newMockRegistryManager("incubator")
-	_, err := initManager("doubleNew", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
-	if err != nil {
-		t.Fatalf("Failed to init cluster spec: %v", err)
-	}
-
-	targetErr := fmt.Sprintf("Could not create app; directory '%s' already exists", appPath)
-	_, err = initManager("doubleNew", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
-	if err == nil || err.Error() != targetErr {
-		t.Fatalf("Expected to fail to create app with message '%s', got '%s'", targetErr, err.Error())
-	}
+	withFs(func(fs afero.Fs) {
+		specFlag := fmt.Sprintf("file:%s", blankSwagger)
+
+		appPath := "/doubleNew"
+		reg := newMockRegistryManager("incubator")
+		_, err := initManager("doubleNew", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, fs)
+		if err != nil {
+			t.Fatalf("Failed to init cluster spec: %v", err)
+		}
+
+		targetErr := fmt.Sprintf("Could not create app; directory '%s' already exists", appPath)
+		_, err = initManager("doubleNew", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, fs)
+		if err == nil || err.Error() != targetErr {
+			t.Fatalf("Expected to fail to create app with message '%s', got '%s'", targetErr, err.Error())
+		}
+	})
 }
diff --git a/metadata/registry.go b/metadata/registry.go
index 7c8f7652430c91b70fa1fabb088ef6a16be339fc..87a097929e7edbb7d399cf4fcc3b9cf86f68b659 100644
--- a/metadata/registry.go
+++ b/metadata/registry.go
@@ -17,7 +17,7 @@ import (
 // AddRegistry adds a registry with `name`, `protocol`, and `uri` to
 // the current ksonnet application.
 func (m *manager) AddRegistry(name, protocol, uri, version string) (*registry.Spec, error) {
-	appSpec, err := m.AppSpec()
+	appSpec, err := app.Read(m.appFS, m.rootPath)
 	if err != nil {
 		return nil, err
 	}
@@ -75,7 +75,7 @@ func (m *manager) GetRegistry(name string) (*registry.Spec, string, error) {
 
 func (m *manager) GetPackage(registryName, libID string) (*parts.Spec, error) {
 	// Retrieve application specification.
-	appSpec, err := m.AppSpec()
+	appSpec, err := app.Read(m.appFS, m.rootPath)
 	if err != nil {
 		return nil, err
 	}
@@ -109,7 +109,7 @@ func (m *manager) GetPackage(registryName, libID string) (*parts.Spec, error) {
 
 func (m *manager) GetDependency(libName string) (*parts.Spec, error) {
 	// Retrieve application specification.
-	appSpec, err := m.AppSpec()
+	appSpec, err := app.Read(m.appFS, m.rootPath)
 	if err != nil {
 		return nil, err
 	}
@@ -144,7 +144,7 @@ func (m *manager) GetDependency(libName string) (*parts.Spec, error) {
 
 func (m *manager) CacheDependency(registryName, libID, libName, libVersion string) (*parts.Spec, error) {
 	// Retrieve application specification.
-	appSpec, err := m.AppSpec()
+	appSpec, err := app.Read(m.appFS, m.rootPath)
 	if err != nil {
 		return nil, err
 	}
@@ -254,7 +254,7 @@ func (m *manager) GetPrototypesForDependency(registryName, libID string) (protot
 }
 
 func (m *manager) GetAllPrototypes() (prototype.SpecificationSchemas, error) {
-	appSpec, err := m.AppSpec()
+	appSpec, err := app.Read(m.appFS, m.rootPath)
 	if err != nil {
 		return nil, err
 	}
@@ -280,7 +280,7 @@ func (m *manager) registryPath(regManager registry.Manager) string {
 }
 
 func (m *manager) getRegistryManager(registryName string) (registry.Manager, string, error) {
-	appSpec, err := m.AppSpec()
+	appSpec, err := app.Read(m.appFS, m.rootPath)
 	if err != nil {
 		return nil, "", err
 	}
diff --git a/pkg/kubecfg/env.go b/pkg/kubecfg/env.go
index 5338c3df2eae45f2a497eea80981a9a21335eb55..f9bc84a0926cf9428828f019756bd052367d2008 100644
--- a/pkg/kubecfg/env.go
+++ b/pkg/kubecfg/env.go
@@ -21,8 +21,7 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/ksonnet/ksonnet/metadata/app"
-
+	"github.com/ksonnet/ksonnet/env"
 	"github.com/ksonnet/ksonnet/metadata"
 	str "github.com/ksonnet/ksonnet/strings"
 )
@@ -83,9 +82,9 @@ func (c *EnvListCmd) Run(out io.Writer) error {
 		return err
 	}
 
-	var envs []app.EnvironmentSpec
+	var envs []env.Env
 	for _, e := range envMap {
-		envs = append(envs, *e)
+		envs = append(envs, e)
 	}
 
 	// Sort environments by ascending alphabetical name
@@ -101,7 +100,11 @@ func (c *EnvListCmd) Run(out io.Writer) error {
 	}
 
 	for _, env := range envs {
-		rows = append(rows, []string{env.Name, env.KubernetesVersion, env.Destination.Namespace, env.Destination.Server})
+		rows = append(rows, []string{
+			env.Name,
+			env.KubernetesVersion,
+			env.Destination.Namespace(),
+			env.Destination.Server()})
 	}
 
 	formattedEnvsList, err := str.PadRows(rows)
diff --git a/pkg/kubecfg/param.go b/pkg/kubecfg/param.go
index a55639feef296741f6ff1ca92b3ed256fd9b7def..bb80d975207fc3559e55ec1a4fedd455990db9f0 100644
--- a/pkg/kubecfg/param.go
+++ b/pkg/kubecfg/param.go
@@ -24,11 +24,13 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/ksonnet/ksonnet/component"
 	param "github.com/ksonnet/ksonnet/metadata/params"
 	str "github.com/ksonnet/ksonnet/strings"
+	"github.com/olekukonko/tablewriter"
 	"github.com/pkg/errors"
+	"github.com/spf13/afero"
 
-	"github.com/fatih/color"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -113,13 +115,20 @@ const (
 // ParamListCmd stores the information necessary display component or
 // environment parameters
 type ParamListCmd struct {
+	fs        afero.Fs
+	root      string
 	component string
 	env       string
+	nsName    string
 }
 
 // NewParamListCmd acts as a constructor for ParamListCmd.
-func NewParamListCmd(component, env string) *ParamListCmd {
-	return &ParamListCmd{component: component, env: env}
+func NewParamListCmd(component, env, nsName string) *ParamListCmd {
+	return &ParamListCmd{
+		component: component,
+		env:       env,
+		nsName:    nsName,
+	}
 }
 
 // Run executes the displaying of params.
@@ -136,7 +145,7 @@ func (c *ParamListCmd) Run(out io.Writer) error {
 
 	var params map[string]param.Params
 	if len(c.env) != 0 {
-		params, err = manager.GetEnvironmentParams(c.env)
+		params, err = manager.GetEnvironmentParams(c.env, c.nsName)
 		if err != nil {
 			return err
 		}
@@ -210,6 +219,8 @@ func outputParams(params map[string]param.Params, out io.Writer) error {
 // ParamDiffCmd stores the information necessary to diff between environment
 // parameters.
 type ParamDiffCmd struct {
+	fs   afero.Fs
+	root string
 	env1 string
 	env2 string
 
@@ -217,8 +228,14 @@ type ParamDiffCmd struct {
 }
 
 // NewParamDiffCmd acts as a constructor for ParamDiffCmd.
-func NewParamDiffCmd(env1, env2, component string) *ParamDiffCmd {
-	return &ParamDiffCmd{env1: env1, env2: env2, component: component}
+func NewParamDiffCmd(fs afero.Fs, root, env1, env2, componentName string) *ParamDiffCmd {
+	return &ParamDiffCmd{
+		fs:        fs,
+		root:      root,
+		env1:      env1,
+		env2:      env2,
+		component: componentName,
+	}
 }
 
 type paramDiffRecord struct {
@@ -235,19 +252,21 @@ func (c *ParamDiffCmd) Run(out io.Writer) error {
 		return err
 	}
 
-	params1, err := manager.GetEnvironmentParams(c.env1)
+	ns, componentName := component.ExtractNamespacedComponent(c.fs, c.root, c.component)
+
+	params1, err := manager.GetEnvironmentParams(c.env1, ns.Path)
 	if err != nil {
 		return err
 	}
 
-	params2, err := manager.GetEnvironmentParams(c.env2)
+	params2, err := manager.GetEnvironmentParams(c.env2, ns.Path)
 	if err != nil {
 		return err
 	}
 
 	if len(c.component) != 0 {
-		params1 = map[string]param.Params{c.component: params1[c.component]}
-		params2 = map[string]param.Params{c.component: params2[c.component]}
+		params1 = map[string]param.Params{componentName: params1[componentName]}
+		params2 = map[string]param.Params{componentName: params2[componentName]}
 	}
 
 	if reflect.DeepEqual(params1, params2) {
@@ -255,127 +274,97 @@ func (c *ParamDiffCmd) Run(out io.Writer) error {
 		return nil
 	}
 
-	records := diffParams(params1, params2)
-
-	//
-	// Format each component parameter information for pretty printing.
-	// Each component will be outputted alphabetically like the following:
-	//
-	//   COMPONENT PARAM     dev       prod
-	//   bar       name      "bar-dev" "bar"
-	//   foo       replicas  1
-	//
-
-	maxComponentLen := len(paramComponentHeader)
-	for _, k := range records {
-		if l := len(k.component); l > maxComponentLen {
-			maxComponentLen = l
-		}
-	}
+	componentNames := collectComponents(params1, params2)
 
-	maxParamLen := len(paramNameHeader) + maxComponentLen + 1
-	for _, k := range records {
-		if l := len(k.param) + maxComponentLen + 1; l > maxParamLen {
-			maxParamLen = l
-		}
-	}
+	var rows [][]string
+	for _, componentName := range componentNames {
+		paramNames := collectParams(params1[componentName], params2[componentName])
 
-	maxEnvLen := len(c.env1) + maxParamLen + 1
-	for _, k := range records {
-		if l := len(k.value1) + maxParamLen + 1; l > maxEnvLen {
-			maxEnvLen = l
-		}
-	}
+		for _, paramName := range paramNames {
+			var v1, v2 string
+			var ok bool
+			var p param.Params
 
-	componentSpacing := strings.Repeat(" ", maxComponentLen-len(paramComponentHeader)+1)
-	nameSpacing := strings.Repeat(" ", maxParamLen-maxComponentLen-len(paramNameHeader))
-	envSpacing := strings.Repeat(" ", maxEnvLen-maxParamLen-len(c.env1))
-
-	// print headers
-	fmt.Fprintln(out, paramComponentHeader+componentSpacing+
-		paramNameHeader+nameSpacing+c.env1+envSpacing+c.env2)
-	fmt.Fprintln(out, strings.Repeat("=", len(paramComponentHeader))+componentSpacing+
-		strings.Repeat("=", len(paramNameHeader))+nameSpacing+
-		strings.Repeat("=", len(c.env1))+envSpacing+
-		strings.Repeat("=", len(c.env2)))
-
-	// print body
-	for _, k := range records {
-		componentSpacing = strings.Repeat(" ", maxComponentLen-len(k.component)+1)
-		nameSpacing = strings.Repeat(" ", maxParamLen-maxComponentLen-len(k.param))
-		envSpacing = strings.Repeat(" ", maxEnvLen-maxParamLen-len(k.value1))
-		line := fmt.Sprint(k.component + componentSpacing + k.param + nameSpacing + k.value1 + envSpacing + k.value2)
-		if len(k.value1) == 0 {
-			color.New(color.BgGreen).Fprint(out, line)
-			fmt.Fprintln(out)
-		} else if len(k.value2) == 0 {
-			color.New(color.BgRed).Fprint(out, line)
-			fmt.Fprintln(out)
-		} else if k.value1 != k.value2 {
-			color.New(color.BgYellow).Fprint(out, line)
-			fmt.Fprintln(out)
-		} else {
-			fmt.Fprintln(out, line)
+			if p, ok = params1[componentName]; ok {
+				v1 = p[paramName]
+			}
+
+			if p, ok = params2[componentName]; ok {
+				v2 = p[paramName]
+			}
+
+			row := []string{
+				componentName,
+				paramName,
+				v1,
+				v2,
+			}
+
+			rows = append(rows, row)
 		}
 	}
 
+	printTable([]string{"COMPONENT", "PARAM", c.env1, c.env2}, rows)
 	return nil
 }
 
-func diffParams(params1, params2 map[string]param.Params) []*paramDiffRecord {
-	var records []*paramDiffRecord
+func collectComponents(param1, param2 map[string]param.Params) []string {
+	m := make(map[string]bool)
+	for k := range param1 {
+		m[k] = true
+	}
+	for k := range param2 {
+		m[k] = true
+	}
 
-	for c := range params1 {
-		if _, contains := params2[c]; !contains {
-			// env2 doesn't have this component, add all params from env1 for this component
-			for p := range params2[c] {
-				records = addRecord(records, c, p, params1[c][p], "")
-			}
-		} else {
-			// has same component -- need to compare params
-			for p := range params1[c] {
-				if _, hasParam := params2[c][p]; !hasParam {
-					// env2 doesn't have this param, add a record with the param value from env1
-					records = addRecord(records, c, p, params1[c][p], "")
-				} else {
-					// env2 has this param too, add a record with both param values
-					records = addRecord(records, c, p, params1[c][p], params2[c][p])
-				}
-			}
-			// add remaining records for params that env2 has that env1 does not for this component
-			for p := range params2[c] {
-				if _, hasParam := params1[c][p]; !hasParam {
-					records = addRecord(records, c, p, "", params2[c][p])
-				}
-			}
-		}
+	var names []string
+
+	for k := range m {
+		names = append(names, k)
 	}
 
-	// add remaining records where env2 contains a component that env1 does not
-	for c := range params2 {
-		if _, contains := params1[c]; !contains {
-			for p := range params2[c] {
-				records = addRecord(records, c, p, "", params2[c][p])
-			}
-		}
+	sort.Strings(names)
+
+	return names
+}
+
+func collectParams(param1, param2 param.Params) []string {
+	m := make(map[string]bool)
+	for k := range param1 {
+		m[k] = true
+	}
+	for k := range param2 {
+		m[k] = true
 	}
 
-	sort.Slice(records, func(i, j int) bool {
-		if records[i].component == records[j].component {
-			return records[i].param < records[j].param
-		}
-		return records[i].component < records[j].component
-	})
+	var names []string
+
+	for k := range m {
+		names = append(names, k)
+	}
 
-	return records
+	sort.Strings(names)
+
+	return names
 }
 
-func addRecord(records []*paramDiffRecord, component, param, value1, value2 string) []*paramDiffRecord {
-	records = append(records, &paramDiffRecord{
-		component: component,
-		param:     param,
-		value1:    value1,
-		value2:    value2,
-	})
-	return records
+func printTable(headers []string, data [][]string) {
+	headerLens := make([]int, len(headers))
+	for i := range headers {
+		headerLens[i] = len(headers[i])
+	}
+
+	for i := range headerLens {
+		headers[i] = fmt.Sprintf("%s\n%s", headers[i], strings.Repeat("=", headerLens[i]))
+	}
+
+	table := tablewriter.NewWriter(os.Stdout)
+	table.SetHeader(headers)
+	table.SetCenterSeparator("")
+	table.SetColumnSeparator("")
+	table.SetRowSeparator("")
+	table.SetRowLine(false)
+	table.SetBorder(false)
+	table.AppendBulk(data)
+	table.Render()
 }
diff --git a/pkg/kubecfg/param_test.go b/pkg/kubecfg/param_test.go
index fb4bf2d742dd563df35c459065ff75584a4cc352..797cbabab3a8ef0dac207fbdc93f8a6629483ec4 100644
--- a/pkg/kubecfg/param_test.go
+++ b/pkg/kubecfg/param_test.go
@@ -18,71 +18,9 @@ package kubecfg
 import (
 	"testing"
 
-	param "github.com/ksonnet/ksonnet/metadata/params"
-
 	"github.com/stretchr/testify/require"
 )
 
-func TestDiffParams(t *testing.T) {
-	tests := []struct {
-		params1  map[string]param.Params
-		params2  map[string]param.Params
-		expected []*paramDiffRecord
-	}{
-		{
-			map[string]param.Params{
-				"bar": param.Params{"replicas": "4"},
-				"foo": param.Params{"replicas": "1", "name": `"foo"`},
-			},
-			map[string]param.Params{
-				"bar": param.Params{"replicas": "3"},
-				"foo": param.Params{"name": `"foo-dev"`, "replicas": "1"},
-				"baz": param.Params{"name": `"baz"`, "replicas": "4"},
-			},
-			[]*paramDiffRecord{
-				&paramDiffRecord{
-					component: "bar",
-					param:     "replicas",
-					value1:    "4",
-					value2:    "3",
-				},
-				&paramDiffRecord{
-					component: "baz",
-					param:     "name",
-					value1:    "",
-					value2:    `"baz"`,
-				},
-				&paramDiffRecord{
-					component: "baz",
-					param:     "replicas",
-					value1:    "",
-					value2:    "4",
-				},
-				&paramDiffRecord{
-					component: "foo",
-					param:     "name",
-					value1:    `"foo"`,
-					value2:    `"foo-dev"`,
-				},
-				&paramDiffRecord{
-					component: "foo",
-					param:     "replicas",
-					value1:    "1",
-					value2:    "1",
-				},
-			},
-		},
-	}
-
-	for _, s := range tests {
-		records := diffParams(s.params1, s.params2)
-		require.Equal(t, len(records), len(s.expected), "Record lengths not equivalent")
-		for i, record := range records {
-			require.EqualValues(t, *s.expected[i], *record)
-		}
-	}
-}
-
 func TestSanitizeParamValue(t *testing.T) {
 	tests := []struct {
 		value    string
diff --git a/testdata/testapp/app.yaml b/testdata/testapp/app.yaml
index 9dc2c5f341d6433113d43bf901fb10c5c3dea883..7a011823bb4106a8bd7ceca50364414b9a92c896 100644
--- a/testdata/testapp/app.yaml
+++ b/testdata/testapp/app.yaml
@@ -1,4 +1,4 @@
-apiVersion: 0.0.1
+apiVersion: 0.1.0
 kind: ksonnet.io/app
 name: test-app
 registries:
diff --git a/vendor/github.com/mattn/go-runewidth/.travis.yml b/vendor/github.com/mattn/go-runewidth/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5c9c2a30f07802ba4e00daca85d1643d7f2be868
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/.travis.yml
@@ -0,0 +1,8 @@
+language: go
+go:
+  - tip
+before_install:
+  - go get github.com/mattn/goveralls
+  - go get golang.org/x/tools/cmd/cover
+script:
+    - $HOME/gopath/bin/goveralls -repotoken lAKAWPzcGsD3A8yBX3BGGtRUdJ6CaGERL
diff --git a/vendor/github.com/mattn/go-runewidth/LICENSE b/vendor/github.com/mattn/go-runewidth/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..91b5cef30ebdf08cb6efe669497a96f58c66035d
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Yasuhiro Matsumoto
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/mattn/go-runewidth/README.mkd b/vendor/github.com/mattn/go-runewidth/README.mkd
new file mode 100644
index 0000000000000000000000000000000000000000..66663a94b0b4e080351c2e39e531336bb10b809c
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/README.mkd
@@ -0,0 +1,27 @@
+go-runewidth
+============
+
+[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth)
+[![Coverage Status](https://coveralls.io/repos/mattn/go-runewidth/badge.png?branch=HEAD)](https://coveralls.io/r/mattn/go-runewidth?branch=HEAD)
+[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth)
+[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth)
+
+Provides functions to get fixed width of the character or string.
+
+Usage
+-----
+
+```go
+runewidth.StringWidth("つのだ☆HIRO") == 12
+```
+
+
+Author
+------
+
+Yasuhiro Matsumoto
+
+License
+-------
+
+under the MIT License: http://mattn.mit-license.org/2013
diff --git a/vendor/github.com/mattn/go-runewidth/runewidth.go b/vendor/github.com/mattn/go-runewidth/runewidth.go
new file mode 100644
index 0000000000000000000000000000000000000000..2164497ad95e6d71bb0c6885bca93218e23238ea
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/runewidth.go
@@ -0,0 +1,1223 @@
+package runewidth
+
+var (
+	// EastAsianWidth will be set true if the current locale is CJK
+	EastAsianWidth = IsEastAsian()
+
+	// DefaultCondition is a condition in current locale
+	DefaultCondition = &Condition{EastAsianWidth}
+)
+
+type interval struct {
+	first rune
+	last  rune
+}
+
+type table []interval
+
+func inTables(r rune, ts ...table) bool {
+	for _, t := range ts {
+		if inTable(r, t) {
+			return true
+		}
+	}
+	return false
+}
+
+func inTable(r rune, t table) bool {
+	// func (t table) IncludesRune(r rune) bool {
+	if r < t[0].first {
+		return false
+	}
+
+	bot := 0
+	top := len(t) - 1
+	for top >= bot {
+		mid := (bot + top) / 2
+
+		switch {
+		case t[mid].last < r:
+			bot = mid + 1
+		case t[mid].first > r:
+			top = mid - 1
+		default:
+			return true
+		}
+	}
+
+	return false
+}
+
+var private = table{
+	{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
+}
+
+var nonprint = table{
+	{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
+	{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
+	{0x202A, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
+	{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
+}
+
+var combining = table{
+	{0x0300, 0x036F}, {0x0483, 0x0489}, {0x0591, 0x05BD},
+	{0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5},
+	{0x05C7, 0x05C7}, {0x0610, 0x061A}, {0x064B, 0x065F},
+	{0x0670, 0x0670}, {0x06D6, 0x06DC}, {0x06DF, 0x06E4},
+	{0x06E7, 0x06E8}, {0x06EA, 0x06ED}, {0x0711, 0x0711},
+	{0x0730, 0x074A}, {0x07A6, 0x07B0}, {0x07EB, 0x07F3},
+	{0x0816, 0x0819}, {0x081B, 0x0823}, {0x0825, 0x0827},
+	{0x0829, 0x082D}, {0x0859, 0x085B}, {0x08D4, 0x08E1},
+	{0x08E3, 0x0903}, {0x093A, 0x093C}, {0x093E, 0x094F},
+	{0x0951, 0x0957}, {0x0962, 0x0963}, {0x0981, 0x0983},
+	{0x09BC, 0x09BC}, {0x09BE, 0x09C4}, {0x09C7, 0x09C8},
+	{0x09CB, 0x09CD}, {0x09D7, 0x09D7}, {0x09E2, 0x09E3},
+	{0x0A01, 0x0A03}, {0x0A3C, 0x0A3C}, {0x0A3E, 0x0A42},
+	{0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51},
+	{0x0A70, 0x0A71}, {0x0A75, 0x0A75}, {0x0A81, 0x0A83},
+	{0x0ABC, 0x0ABC}, {0x0ABE, 0x0AC5}, {0x0AC7, 0x0AC9},
+	{0x0ACB, 0x0ACD}, {0x0AE2, 0x0AE3}, {0x0B01, 0x0B03},
+	{0x0B3C, 0x0B3C}, {0x0B3E, 0x0B44}, {0x0B47, 0x0B48},
+	{0x0B4B, 0x0B4D}, {0x0B56, 0x0B57}, {0x0B62, 0x0B63},
+	{0x0B82, 0x0B82}, {0x0BBE, 0x0BC2}, {0x0BC6, 0x0BC8},
+	{0x0BCA, 0x0BCD}, {0x0BD7, 0x0BD7}, {0x0C00, 0x0C03},
+	{0x0C3E, 0x0C44}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D},
+	{0x0C55, 0x0C56}, {0x0C62, 0x0C63}, {0x0C81, 0x0C83},
+	{0x0CBC, 0x0CBC}, {0x0CBE, 0x0CC4}, {0x0CC6, 0x0CC8},
+	{0x0CCA, 0x0CCD}, {0x0CD5, 0x0CD6}, {0x0CE2, 0x0CE3},
+	{0x0D01, 0x0D03}, {0x0D3E, 0x0D44}, {0x0D46, 0x0D48},
+	{0x0D4A, 0x0D4D}, {0x0D57, 0x0D57}, {0x0D62, 0x0D63},
+	{0x0D82, 0x0D83}, {0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4},
+	{0x0DD6, 0x0DD6}, {0x0DD8, 0x0DDF}, {0x0DF2, 0x0DF3},
+	{0x0E31, 0x0E31}, {0x0E34, 0x0E3A}, {0x0E47, 0x0E4E},
+	{0x0EB1, 0x0EB1}, {0x0EB4, 0x0EB9}, {0x0EBB, 0x0EBC},
+	{0x0EC8, 0x0ECD}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35},
+	{0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F3E, 0x0F3F},
+	{0x0F71, 0x0F84}, {0x0F86, 0x0F87}, {0x0F8D, 0x0F97},
+	{0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102B, 0x103E},
+	{0x1056, 0x1059}, {0x105E, 0x1060}, {0x1062, 0x1064},
+	{0x1067, 0x106D}, {0x1071, 0x1074}, {0x1082, 0x108D},
+	{0x108F, 0x108F}, {0x109A, 0x109D}, {0x135D, 0x135F},
+	{0x1712, 0x1714}, {0x1732, 0x1734}, {0x1752, 0x1753},
+	{0x1772, 0x1773}, {0x17B4, 0x17D3}, {0x17DD, 0x17DD},
+	{0x180B, 0x180D}, {0x1885, 0x1886}, {0x18A9, 0x18A9},
+	{0x1920, 0x192B}, {0x1930, 0x193B}, {0x1A17, 0x1A1B},
+	{0x1A55, 0x1A5E}, {0x1A60, 0x1A7C}, {0x1A7F, 0x1A7F},
+	{0x1AB0, 0x1ABE}, {0x1B00, 0x1B04}, {0x1B34, 0x1B44},
+	{0x1B6B, 0x1B73}, {0x1B80, 0x1B82}, {0x1BA1, 0x1BAD},
+	{0x1BE6, 0x1BF3}, {0x1C24, 0x1C37}, {0x1CD0, 0x1CD2},
+	{0x1CD4, 0x1CE8}, {0x1CED, 0x1CED}, {0x1CF2, 0x1CF4},
+	{0x1CF8, 0x1CF9}, {0x1DC0, 0x1DF5}, {0x1DFB, 0x1DFF},
+	{0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2D7F, 0x2D7F},
+	{0x2DE0, 0x2DFF}, {0x302A, 0x302F}, {0x3099, 0x309A},
+	{0xA66F, 0xA672}, {0xA674, 0xA67D}, {0xA69E, 0xA69F},
+	{0xA6F0, 0xA6F1}, {0xA802, 0xA802}, {0xA806, 0xA806},
+	{0xA80B, 0xA80B}, {0xA823, 0xA827}, {0xA880, 0xA881},
+	{0xA8B4, 0xA8C5}, {0xA8E0, 0xA8F1}, {0xA926, 0xA92D},
+	{0xA947, 0xA953}, {0xA980, 0xA983}, {0xA9B3, 0xA9C0},
+	{0xA9E5, 0xA9E5}, {0xAA29, 0xAA36}, {0xAA43, 0xAA43},
+	{0xAA4C, 0xAA4D}, {0xAA7B, 0xAA7D}, {0xAAB0, 0xAAB0},
+	{0xAAB2, 0xAAB4}, {0xAAB7, 0xAAB8}, {0xAABE, 0xAABF},
+	{0xAAC1, 0xAAC1}, {0xAAEB, 0xAAEF}, {0xAAF5, 0xAAF6},
+	{0xABE3, 0xABEA}, {0xABEC, 0xABED}, {0xFB1E, 0xFB1E},
+	{0xFE00, 0xFE0F}, {0xFE20, 0xFE2F}, {0x101FD, 0x101FD},
+	{0x102E0, 0x102E0}, {0x10376, 0x1037A}, {0x10A01, 0x10A03},
+	{0x10A05, 0x10A06}, {0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A},
+	{0x10A3F, 0x10A3F}, {0x10AE5, 0x10AE6}, {0x11000, 0x11002},
+	{0x11038, 0x11046}, {0x1107F, 0x11082}, {0x110B0, 0x110BA},
+	{0x11100, 0x11102}, {0x11127, 0x11134}, {0x11173, 0x11173},
+	{0x11180, 0x11182}, {0x111B3, 0x111C0}, {0x111CA, 0x111CC},
+	{0x1122C, 0x11237}, {0x1123E, 0x1123E}, {0x112DF, 0x112EA},
+	{0x11300, 0x11303}, {0x1133C, 0x1133C}, {0x1133E, 0x11344},
+	{0x11347, 0x11348}, {0x1134B, 0x1134D}, {0x11357, 0x11357},
+	{0x11362, 0x11363}, {0x11366, 0x1136C}, {0x11370, 0x11374},
+	{0x11435, 0x11446}, {0x114B0, 0x114C3}, {0x115AF, 0x115B5},
+	{0x115B8, 0x115C0}, {0x115DC, 0x115DD}, {0x11630, 0x11640},
+	{0x116AB, 0x116B7}, {0x1171D, 0x1172B}, {0x11C2F, 0x11C36},
+	{0x11C38, 0x11C3F}, {0x11C92, 0x11CA7}, {0x11CA9, 0x11CB6},
+	{0x16AF0, 0x16AF4}, {0x16B30, 0x16B36}, {0x16F51, 0x16F7E},
+	{0x16F8F, 0x16F92}, {0x1BC9D, 0x1BC9E}, {0x1D165, 0x1D169},
+	{0x1D16D, 0x1D172}, {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B},
+	{0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0x1DA00, 0x1DA36},
+	{0x1DA3B, 0x1DA6C}, {0x1DA75, 0x1DA75}, {0x1DA84, 0x1DA84},
+	{0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006},
+	{0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024},
+	{0x1E026, 0x1E02A}, {0x1E8D0, 0x1E8D6}, {0x1E944, 0x1E94A},
+	{0xE0100, 0xE01EF},
+}
+
+var doublewidth = table{
+	{0x1100, 0x115F}, {0x231A, 0x231B}, {0x2329, 0x232A},
+	{0x23E9, 0x23EC}, {0x23F0, 0x23F0}, {0x23F3, 0x23F3},
+	{0x25FD, 0x25FE}, {0x2614, 0x2615}, {0x2648, 0x2653},
+	{0x267F, 0x267F}, {0x2693, 0x2693}, {0x26A1, 0x26A1},
+	{0x26AA, 0x26AB}, {0x26BD, 0x26BE}, {0x26C4, 0x26C5},
+	{0x26CE, 0x26CE}, {0x26D4, 0x26D4}, {0x26EA, 0x26EA},
+	{0x26F2, 0x26F3}, {0x26F5, 0x26F5}, {0x26FA, 0x26FA},
+	{0x26FD, 0x26FD}, {0x2705, 0x2705}, {0x270A, 0x270B},
+	{0x2728, 0x2728}, {0x274C, 0x274C}, {0x274E, 0x274E},
+	{0x2753, 0x2755}, {0x2757, 0x2757}, {0x2795, 0x2797},
+	{0x27B0, 0x27B0}, {0x27BF, 0x27BF}, {0x2B1B, 0x2B1C},
+	{0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x2E80, 0x2E99},
+	{0x2E9B, 0x2EF3}, {0x2F00, 0x2FD5}, {0x2FF0, 0x2FFB},
+	{0x3000, 0x303E}, {0x3041, 0x3096}, {0x3099, 0x30FF},
+	{0x3105, 0x312D}, {0x3131, 0x318E}, {0x3190, 0x31BA},
+	{0x31C0, 0x31E3}, {0x31F0, 0x321E}, {0x3220, 0x3247},
+	{0x3250, 0x32FE}, {0x3300, 0x4DBF}, {0x4E00, 0xA48C},
+	{0xA490, 0xA4C6}, {0xA960, 0xA97C}, {0xAC00, 0xD7A3},
+	{0xF900, 0xFAFF}, {0xFE10, 0xFE19}, {0xFE30, 0xFE52},
+	{0xFE54, 0xFE66}, {0xFE68, 0xFE6B}, {0xFF01, 0xFF60},
+	{0xFFE0, 0xFFE6}, {0x16FE0, 0x16FE0}, {0x17000, 0x187EC},
+	{0x18800, 0x18AF2}, {0x1B000, 0x1B001}, {0x1F004, 0x1F004},
+	{0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A},
+	{0x1F200, 0x1F202}, {0x1F210, 0x1F23B}, {0x1F240, 0x1F248},
+	{0x1F250, 0x1F251}, {0x1F300, 0x1F320}, {0x1F32D, 0x1F335},
+	{0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, {0x1F3A0, 0x1F3CA},
+	{0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, {0x1F3F4, 0x1F3F4},
+	{0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, {0x1F442, 0x1F4FC},
+	{0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, {0x1F550, 0x1F567},
+	{0x1F57A, 0x1F57A}, {0x1F595, 0x1F596}, {0x1F5A4, 0x1F5A4},
+	{0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5}, {0x1F6CC, 0x1F6CC},
+	{0x1F6D0, 0x1F6D2}, {0x1F6EB, 0x1F6EC}, {0x1F6F4, 0x1F6F6},
+	{0x1F910, 0x1F91E}, {0x1F920, 0x1F927}, {0x1F930, 0x1F930},
+	{0x1F933, 0x1F93E}, {0x1F940, 0x1F94B}, {0x1F950, 0x1F95E},
+	{0x1F980, 0x1F991}, {0x1F9C0, 0x1F9C0}, {0x20000, 0x2FFFD},
+	{0x30000, 0x3FFFD},
+}
+
+var ambiguous = table{
+	{0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8},
+	{0x00AA, 0x00AA}, {0x00AD, 0x00AE}, {0x00B0, 0x00B4},
+	{0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6},
+	{0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1},
+	{0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED},
+	{0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA},
+	{0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101},
+	{0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B},
+	{0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133},
+	{0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144},
+	{0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153},
+	{0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE},
+	{0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4},
+	{0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA},
+	{0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261},
+	{0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB},
+	{0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB},
+	{0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0300, 0x036F},
+	{0x0391, 0x03A1}, {0x03A3, 0x03A9}, {0x03B1, 0x03C1},
+	{0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F},
+	{0x0451, 0x0451}, {0x2010, 0x2010}, {0x2013, 0x2016},
+	{0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022},
+	{0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033},
+	{0x2035, 0x2035}, {0x203B, 0x203B}, {0x203E, 0x203E},
+	{0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084},
+	{0x20AC, 0x20AC}, {0x2103, 0x2103}, {0x2105, 0x2105},
+	{0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116},
+	{0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B},
+	{0x2153, 0x2154}, {0x215B, 0x215E}, {0x2160, 0x216B},
+	{0x2170, 0x2179}, {0x2189, 0x2189}, {0x2190, 0x2199},
+	{0x21B8, 0x21B9}, {0x21D2, 0x21D2}, {0x21D4, 0x21D4},
+	{0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203},
+	{0x2207, 0x2208}, {0x220B, 0x220B}, {0x220F, 0x220F},
+	{0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A},
+	{0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225},
+	{0x2227, 0x222C}, {0x222E, 0x222E}, {0x2234, 0x2237},
+	{0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C},
+	{0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267},
+	{0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283},
+	{0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299},
+	{0x22A5, 0x22A5}, {0x22BF, 0x22BF}, {0x2312, 0x2312},
+	{0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573},
+	{0x2580, 0x258F}, {0x2592, 0x2595}, {0x25A0, 0x25A1},
+	{0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7},
+	{0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8},
+	{0x25CB, 0x25CB}, {0x25CE, 0x25D1}, {0x25E2, 0x25E5},
+	{0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609},
+	{0x260E, 0x260F}, {0x261C, 0x261C}, {0x261E, 0x261E},
+	{0x2640, 0x2640}, {0x2642, 0x2642}, {0x2660, 0x2661},
+	{0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D},
+	{0x266F, 0x266F}, {0x269E, 0x269F}, {0x26BF, 0x26BF},
+	{0x26C6, 0x26CD}, {0x26CF, 0x26D3}, {0x26D5, 0x26E1},
+	{0x26E3, 0x26E3}, {0x26E8, 0x26E9}, {0x26EB, 0x26F1},
+	{0x26F4, 0x26F4}, {0x26F6, 0x26F9}, {0x26FB, 0x26FC},
+	{0x26FE, 0x26FF}, {0x273D, 0x273D}, {0x2776, 0x277F},
+	{0x2B56, 0x2B59}, {0x3248, 0x324F}, {0xE000, 0xF8FF},
+	{0xFE00, 0xFE0F}, {0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A},
+	{0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D},
+	{0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xE0100, 0xE01EF},
+	{0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD},
+}
+
+var emoji = table{
+	{0x1F1E6, 0x1F1FF}, {0x1F321, 0x1F321}, {0x1F324, 0x1F32C},
+	{0x1F336, 0x1F336}, {0x1F37D, 0x1F37D}, {0x1F396, 0x1F397},
+	{0x1F399, 0x1F39B}, {0x1F39E, 0x1F39F}, {0x1F3CB, 0x1F3CE},
+	{0x1F3D4, 0x1F3DF}, {0x1F3F3, 0x1F3F5}, {0x1F3F7, 0x1F3F7},
+	{0x1F43F, 0x1F43F}, {0x1F441, 0x1F441}, {0x1F4FD, 0x1F4FD},
+	{0x1F549, 0x1F54A}, {0x1F56F, 0x1F570}, {0x1F573, 0x1F579},
+	{0x1F587, 0x1F587}, {0x1F58A, 0x1F58D}, {0x1F590, 0x1F590},
+	{0x1F5A5, 0x1F5A5}, {0x1F5A8, 0x1F5A8}, {0x1F5B1, 0x1F5B2},
+	{0x1F5BC, 0x1F5BC}, {0x1F5C2, 0x1F5C4}, {0x1F5D1, 0x1F5D3},
+	{0x1F5DC, 0x1F5DE}, {0x1F5E1, 0x1F5E1}, {0x1F5E3, 0x1F5E3},
+	{0x1F5E8, 0x1F5E8}, {0x1F5EF, 0x1F5EF}, {0x1F5F3, 0x1F5F3},
+	{0x1F5FA, 0x1F5FA}, {0x1F6CB, 0x1F6CF}, {0x1F6E0, 0x1F6E5},
+	{0x1F6E9, 0x1F6E9}, {0x1F6F0, 0x1F6F0}, {0x1F6F3, 0x1F6F3},
+}
+
+var notassigned = table{
+	{0x0378, 0x0379}, {0x0380, 0x0383}, {0x038B, 0x038B},
+	{0x038D, 0x038D}, {0x03A2, 0x03A2}, {0x0530, 0x0530},
+	{0x0557, 0x0558}, {0x0560, 0x0560}, {0x0588, 0x0588},
+	{0x058B, 0x058C}, {0x0590, 0x0590}, {0x05C8, 0x05CF},
+	{0x05EB, 0x05EF}, {0x05F5, 0x05FF}, {0x061D, 0x061D},
+	{0x070E, 0x070E}, {0x074B, 0x074C}, {0x07B2, 0x07BF},
+	{0x07FB, 0x07FF}, {0x082E, 0x082F}, {0x083F, 0x083F},
+	{0x085C, 0x085D}, {0x085F, 0x089F}, {0x08B5, 0x08B5},
+	{0x08BE, 0x08D3}, {0x0984, 0x0984}, {0x098D, 0x098E},
+	{0x0991, 0x0992}, {0x09A9, 0x09A9}, {0x09B1, 0x09B1},
+	{0x09B3, 0x09B5}, {0x09BA, 0x09BB}, {0x09C5, 0x09C6},
+	{0x09C9, 0x09CA}, {0x09CF, 0x09D6}, {0x09D8, 0x09DB},
+	{0x09DE, 0x09DE}, {0x09E4, 0x09E5}, {0x09FC, 0x0A00},
+	{0x0A04, 0x0A04}, {0x0A0B, 0x0A0E}, {0x0A11, 0x0A12},
+	{0x0A29, 0x0A29}, {0x0A31, 0x0A31}, {0x0A34, 0x0A34},
+	{0x0A37, 0x0A37}, {0x0A3A, 0x0A3B}, {0x0A3D, 0x0A3D},
+	{0x0A43, 0x0A46}, {0x0A49, 0x0A4A}, {0x0A4E, 0x0A50},
+	{0x0A52, 0x0A58}, {0x0A5D, 0x0A5D}, {0x0A5F, 0x0A65},
+	{0x0A76, 0x0A80}, {0x0A84, 0x0A84}, {0x0A8E, 0x0A8E},
+	{0x0A92, 0x0A92}, {0x0AA9, 0x0AA9}, {0x0AB1, 0x0AB1},
+	{0x0AB4, 0x0AB4}, {0x0ABA, 0x0ABB}, {0x0AC6, 0x0AC6},
+	{0x0ACA, 0x0ACA}, {0x0ACE, 0x0ACF}, {0x0AD1, 0x0ADF},
+	{0x0AE4, 0x0AE5}, {0x0AF2, 0x0AF8}, {0x0AFA, 0x0B00},
+	{0x0B04, 0x0B04}, {0x0B0D, 0x0B0E}, {0x0B11, 0x0B12},
+	{0x0B29, 0x0B29}, {0x0B31, 0x0B31}, {0x0B34, 0x0B34},
+	{0x0B3A, 0x0B3B}, {0x0B45, 0x0B46}, {0x0B49, 0x0B4A},
+	{0x0B4E, 0x0B55}, {0x0B58, 0x0B5B}, {0x0B5E, 0x0B5E},
+	{0x0B64, 0x0B65}, {0x0B78, 0x0B81}, {0x0B84, 0x0B84},
+	{0x0B8B, 0x0B8D}, {0x0B91, 0x0B91}, {0x0B96, 0x0B98},
+	{0x0B9B, 0x0B9B}, {0x0B9D, 0x0B9D}, {0x0BA0, 0x0BA2},
+	{0x0BA5, 0x0BA7}, {0x0BAB, 0x0BAD}, {0x0BBA, 0x0BBD},
+	{0x0BC3, 0x0BC5}, {0x0BC9, 0x0BC9}, {0x0BCE, 0x0BCF},
+	{0x0BD1, 0x0BD6}, {0x0BD8, 0x0BE5}, {0x0BFB, 0x0BFF},
+	{0x0C04, 0x0C04}, {0x0C0D, 0x0C0D}, {0x0C11, 0x0C11},
+	{0x0C29, 0x0C29}, {0x0C3A, 0x0C3C}, {0x0C45, 0x0C45},
+	{0x0C49, 0x0C49}, {0x0C4E, 0x0C54}, {0x0C57, 0x0C57},
+	{0x0C5B, 0x0C5F}, {0x0C64, 0x0C65}, {0x0C70, 0x0C77},
+	{0x0C84, 0x0C84}, {0x0C8D, 0x0C8D}, {0x0C91, 0x0C91},
+	{0x0CA9, 0x0CA9}, {0x0CB4, 0x0CB4}, {0x0CBA, 0x0CBB},
+	{0x0CC5, 0x0CC5}, {0x0CC9, 0x0CC9}, {0x0CCE, 0x0CD4},
+	{0x0CD7, 0x0CDD}, {0x0CDF, 0x0CDF}, {0x0CE4, 0x0CE5},
+	{0x0CF0, 0x0CF0}, {0x0CF3, 0x0D00}, {0x0D04, 0x0D04},
+	{0x0D0D, 0x0D0D}, {0x0D11, 0x0D11}, {0x0D3B, 0x0D3C},
+	{0x0D45, 0x0D45}, {0x0D49, 0x0D49}, {0x0D50, 0x0D53},
+	{0x0D64, 0x0D65}, {0x0D80, 0x0D81}, {0x0D84, 0x0D84},
+	{0x0D97, 0x0D99}, {0x0DB2, 0x0DB2}, {0x0DBC, 0x0DBC},
+	{0x0DBE, 0x0DBF}, {0x0DC7, 0x0DC9}, {0x0DCB, 0x0DCE},
+	{0x0DD5, 0x0DD5}, {0x0DD7, 0x0DD7}, {0x0DE0, 0x0DE5},
+	{0x0DF0, 0x0DF1}, {0x0DF5, 0x0E00}, {0x0E3B, 0x0E3E},
+	{0x0E5C, 0x0E80}, {0x0E83, 0x0E83}, {0x0E85, 0x0E86},
+	{0x0E89, 0x0E89}, {0x0E8B, 0x0E8C}, {0x0E8E, 0x0E93},
+	{0x0E98, 0x0E98}, {0x0EA0, 0x0EA0}, {0x0EA4, 0x0EA4},
+	{0x0EA6, 0x0EA6}, {0x0EA8, 0x0EA9}, {0x0EAC, 0x0EAC},
+	{0x0EBA, 0x0EBA}, {0x0EBE, 0x0EBF}, {0x0EC5, 0x0EC5},
+	{0x0EC7, 0x0EC7}, {0x0ECE, 0x0ECF}, {0x0EDA, 0x0EDB},
+	{0x0EE0, 0x0EFF}, {0x0F48, 0x0F48}, {0x0F6D, 0x0F70},
+	{0x0F98, 0x0F98}, {0x0FBD, 0x0FBD}, {0x0FCD, 0x0FCD},
+	{0x0FDB, 0x0FFF}, {0x10C6, 0x10C6}, {0x10C8, 0x10CC},
+	{0x10CE, 0x10CF}, {0x1249, 0x1249}, {0x124E, 0x124F},
+	{0x1257, 0x1257}, {0x1259, 0x1259}, {0x125E, 0x125F},
+	{0x1289, 0x1289}, {0x128E, 0x128F}, {0x12B1, 0x12B1},
+	{0x12B6, 0x12B7}, {0x12BF, 0x12BF}, {0x12C1, 0x12C1},
+	{0x12C6, 0x12C7}, {0x12D7, 0x12D7}, {0x1311, 0x1311},
+	{0x1316, 0x1317}, {0x135B, 0x135C}, {0x137D, 0x137F},
+	{0x139A, 0x139F}, {0x13F6, 0x13F7}, {0x13FE, 0x13FF},
+	{0x169D, 0x169F}, {0x16F9, 0x16FF}, {0x170D, 0x170D},
+	{0x1715, 0x171F}, {0x1737, 0x173F}, {0x1754, 0x175F},
+	{0x176D, 0x176D}, {0x1771, 0x1771}, {0x1774, 0x177F},
+	{0x17DE, 0x17DF}, {0x17EA, 0x17EF}, {0x17FA, 0x17FF},
+	{0x180F, 0x180F}, {0x181A, 0x181F}, {0x1878, 0x187F},
+	{0x18AB, 0x18AF}, {0x18F6, 0x18FF}, {0x191F, 0x191F},
+	{0x192C, 0x192F}, {0x193C, 0x193F}, {0x1941, 0x1943},
+	{0x196E, 0x196F}, {0x1975, 0x197F}, {0x19AC, 0x19AF},
+	{0x19CA, 0x19CF}, {0x19DB, 0x19DD}, {0x1A1C, 0x1A1D},
+	{0x1A5F, 0x1A5F}, {0x1A7D, 0x1A7E}, {0x1A8A, 0x1A8F},
+	{0x1A9A, 0x1A9F}, {0x1AAE, 0x1AAF}, {0x1ABF, 0x1AFF},
+	{0x1B4C, 0x1B4F}, {0x1B7D, 0x1B7F}, {0x1BF4, 0x1BFB},
+	{0x1C38, 0x1C3A}, {0x1C4A, 0x1C4C}, {0x1C89, 0x1CBF},
+	{0x1CC8, 0x1CCF}, {0x1CF7, 0x1CF7}, {0x1CFA, 0x1CFF},
+	{0x1DF6, 0x1DFA}, {0x1F16, 0x1F17}, {0x1F1E, 0x1F1F},
+	{0x1F46, 0x1F47}, {0x1F4E, 0x1F4F}, {0x1F58, 0x1F58},
+	{0x1F5A, 0x1F5A}, {0x1F5C, 0x1F5C}, {0x1F5E, 0x1F5E},
+	{0x1F7E, 0x1F7F}, {0x1FB5, 0x1FB5}, {0x1FC5, 0x1FC5},
+	{0x1FD4, 0x1FD5}, {0x1FDC, 0x1FDC}, {0x1FF0, 0x1FF1},
+	{0x1FF5, 0x1FF5}, {0x1FFF, 0x1FFF}, {0x2065, 0x2065},
+	{0x2072, 0x2073}, {0x208F, 0x208F}, {0x209D, 0x209F},
+	{0x20BF, 0x20CF}, {0x20F1, 0x20FF}, {0x218C, 0x218F},
+	{0x23FF, 0x23FF}, {0x2427, 0x243F}, {0x244B, 0x245F},
+	{0x2B74, 0x2B75}, {0x2B96, 0x2B97}, {0x2BBA, 0x2BBC},
+	{0x2BC9, 0x2BC9}, {0x2BD2, 0x2BEB}, {0x2BF0, 0x2BFF},
+	{0x2C2F, 0x2C2F}, {0x2C5F, 0x2C5F}, {0x2CF4, 0x2CF8},
+	{0x2D26, 0x2D26}, {0x2D28, 0x2D2C}, {0x2D2E, 0x2D2F},
+	{0x2D68, 0x2D6E}, {0x2D71, 0x2D7E}, {0x2D97, 0x2D9F},
+	{0x2DA7, 0x2DA7}, {0x2DAF, 0x2DAF}, {0x2DB7, 0x2DB7},
+	{0x2DBF, 0x2DBF}, {0x2DC7, 0x2DC7}, {0x2DCF, 0x2DCF},
+	{0x2DD7, 0x2DD7}, {0x2DDF, 0x2DDF}, {0x2E45, 0x2E7F},
+	{0x2E9A, 0x2E9A}, {0x2EF4, 0x2EFF}, {0x2FD6, 0x2FEF},
+	{0x2FFC, 0x2FFF}, {0x3040, 0x3040}, {0x3097, 0x3098},
+	{0x3100, 0x3104}, {0x312E, 0x3130}, {0x318F, 0x318F},
+	{0x31BB, 0x31BF}, {0x31E4, 0x31EF}, {0x321F, 0x321F},
+	{0x32FF, 0x32FF}, {0x4DB6, 0x4DBF}, {0x9FD6, 0x9FFF},
+	{0xA48D, 0xA48F}, {0xA4C7, 0xA4CF}, {0xA62C, 0xA63F},
+	{0xA6F8, 0xA6FF}, {0xA7AF, 0xA7AF}, {0xA7B8, 0xA7F6},
+	{0xA82C, 0xA82F}, {0xA83A, 0xA83F}, {0xA878, 0xA87F},
+	{0xA8C6, 0xA8CD}, {0xA8DA, 0xA8DF}, {0xA8FE, 0xA8FF},
+	{0xA954, 0xA95E}, {0xA97D, 0xA97F}, {0xA9CE, 0xA9CE},
+	{0xA9DA, 0xA9DD}, {0xA9FF, 0xA9FF}, {0xAA37, 0xAA3F},
+	{0xAA4E, 0xAA4F}, {0xAA5A, 0xAA5B}, {0xAAC3, 0xAADA},
+	{0xAAF7, 0xAB00}, {0xAB07, 0xAB08}, {0xAB0F, 0xAB10},
+	{0xAB17, 0xAB1F}, {0xAB27, 0xAB27}, {0xAB2F, 0xAB2F},
+	{0xAB66, 0xAB6F}, {0xABEE, 0xABEF}, {0xABFA, 0xABFF},
+	{0xD7A4, 0xD7AF}, {0xD7C7, 0xD7CA}, {0xD7FC, 0xD7FF},
+	{0xFA6E, 0xFA6F}, {0xFADA, 0xFAFF}, {0xFB07, 0xFB12},
+	{0xFB18, 0xFB1C}, {0xFB37, 0xFB37}, {0xFB3D, 0xFB3D},
+	{0xFB3F, 0xFB3F}, {0xFB42, 0xFB42}, {0xFB45, 0xFB45},
+	{0xFBC2, 0xFBD2}, {0xFD40, 0xFD4F}, {0xFD90, 0xFD91},
+	{0xFDC8, 0xFDEF}, {0xFDFE, 0xFDFF}, {0xFE1A, 0xFE1F},
+	{0xFE53, 0xFE53}, {0xFE67, 0xFE67}, {0xFE6C, 0xFE6F},
+	{0xFE75, 0xFE75}, {0xFEFD, 0xFEFE}, {0xFF00, 0xFF00},
+	{0xFFBF, 0xFFC1}, {0xFFC8, 0xFFC9}, {0xFFD0, 0xFFD1},
+	{0xFFD8, 0xFFD9}, {0xFFDD, 0xFFDF}, {0xFFE7, 0xFFE7},
+	{0xFFEF, 0xFFF8}, {0xFFFE, 0xFFFF}, {0x1000C, 0x1000C},
+	{0x10027, 0x10027}, {0x1003B, 0x1003B}, {0x1003E, 0x1003E},
+	{0x1004E, 0x1004F}, {0x1005E, 0x1007F}, {0x100FB, 0x100FF},
+	{0x10103, 0x10106}, {0x10134, 0x10136}, {0x1018F, 0x1018F},
+	{0x1019C, 0x1019F}, {0x101A1, 0x101CF}, {0x101FE, 0x1027F},
+	{0x1029D, 0x1029F}, {0x102D1, 0x102DF}, {0x102FC, 0x102FF},
+	{0x10324, 0x1032F}, {0x1034B, 0x1034F}, {0x1037B, 0x1037F},
+	{0x1039E, 0x1039E}, {0x103C4, 0x103C7}, {0x103D6, 0x103FF},
+	{0x1049E, 0x1049F}, {0x104AA, 0x104AF}, {0x104D4, 0x104D7},
+	{0x104FC, 0x104FF}, {0x10528, 0x1052F}, {0x10564, 0x1056E},
+	{0x10570, 0x105FF}, {0x10737, 0x1073F}, {0x10756, 0x1075F},
+	{0x10768, 0x107FF}, {0x10806, 0x10807}, {0x10809, 0x10809},
+	{0x10836, 0x10836}, {0x10839, 0x1083B}, {0x1083D, 0x1083E},
+	{0x10856, 0x10856}, {0x1089F, 0x108A6}, {0x108B0, 0x108DF},
+	{0x108F3, 0x108F3}, {0x108F6, 0x108FA}, {0x1091C, 0x1091E},
+	{0x1093A, 0x1093E}, {0x10940, 0x1097F}, {0x109B8, 0x109BB},
+	{0x109D0, 0x109D1}, {0x10A04, 0x10A04}, {0x10A07, 0x10A0B},
+	{0x10A14, 0x10A14}, {0x10A18, 0x10A18}, {0x10A34, 0x10A37},
+	{0x10A3B, 0x10A3E}, {0x10A48, 0x10A4F}, {0x10A59, 0x10A5F},
+	{0x10AA0, 0x10ABF}, {0x10AE7, 0x10AEA}, {0x10AF7, 0x10AFF},
+	{0x10B36, 0x10B38}, {0x10B56, 0x10B57}, {0x10B73, 0x10B77},
+	{0x10B92, 0x10B98}, {0x10B9D, 0x10BA8}, {0x10BB0, 0x10BFF},
+	{0x10C49, 0x10C7F}, {0x10CB3, 0x10CBF}, {0x10CF3, 0x10CF9},
+	{0x10D00, 0x10E5F}, {0x10E7F, 0x10FFF}, {0x1104E, 0x11051},
+	{0x11070, 0x1107E}, {0x110C2, 0x110CF}, {0x110E9, 0x110EF},
+	{0x110FA, 0x110FF}, {0x11135, 0x11135}, {0x11144, 0x1114F},
+	{0x11177, 0x1117F}, {0x111CE, 0x111CF}, {0x111E0, 0x111E0},
+	{0x111F5, 0x111FF}, {0x11212, 0x11212}, {0x1123F, 0x1127F},
+	{0x11287, 0x11287}, {0x11289, 0x11289}, {0x1128E, 0x1128E},
+	{0x1129E, 0x1129E}, {0x112AA, 0x112AF}, {0x112EB, 0x112EF},
+	{0x112FA, 0x112FF}, {0x11304, 0x11304}, {0x1130D, 0x1130E},
+	{0x11311, 0x11312}, {0x11329, 0x11329}, {0x11331, 0x11331},
+	{0x11334, 0x11334}, {0x1133A, 0x1133B}, {0x11345, 0x11346},
+	{0x11349, 0x1134A}, {0x1134E, 0x1134F}, {0x11351, 0x11356},
+	{0x11358, 0x1135C}, {0x11364, 0x11365}, {0x1136D, 0x1136F},
+	{0x11375, 0x113FF}, {0x1145A, 0x1145A}, {0x1145C, 0x1145C},
+	{0x1145E, 0x1147F}, {0x114C8, 0x114CF}, {0x114DA, 0x1157F},
+	{0x115B6, 0x115B7}, {0x115DE, 0x115FF}, {0x11645, 0x1164F},
+	{0x1165A, 0x1165F}, {0x1166D, 0x1167F}, {0x116B8, 0x116BF},
+	{0x116CA, 0x116FF}, {0x1171A, 0x1171C}, {0x1172C, 0x1172F},
+	{0x11740, 0x1189F}, {0x118F3, 0x118FE}, {0x11900, 0x11ABF},
+	{0x11AF9, 0x11BFF}, {0x11C09, 0x11C09}, {0x11C37, 0x11C37},
+	{0x11C46, 0x11C4F}, {0x11C6D, 0x11C6F}, {0x11C90, 0x11C91},
+	{0x11CA8, 0x11CA8}, {0x11CB7, 0x11FFF}, {0x1239A, 0x123FF},
+	{0x1246F, 0x1246F}, {0x12475, 0x1247F}, {0x12544, 0x12FFF},
+	{0x1342F, 0x143FF}, {0x14647, 0x167FF}, {0x16A39, 0x16A3F},
+	{0x16A5F, 0x16A5F}, {0x16A6A, 0x16A6D}, {0x16A70, 0x16ACF},
+	{0x16AEE, 0x16AEF}, {0x16AF6, 0x16AFF}, {0x16B46, 0x16B4F},
+	{0x16B5A, 0x16B5A}, {0x16B62, 0x16B62}, {0x16B78, 0x16B7C},
+	{0x16B90, 0x16EFF}, {0x16F45, 0x16F4F}, {0x16F7F, 0x16F8E},
+	{0x16FA0, 0x16FDF}, {0x16FE1, 0x16FFF}, {0x187ED, 0x187FF},
+	{0x18AF3, 0x1AFFF}, {0x1B002, 0x1BBFF}, {0x1BC6B, 0x1BC6F},
+	{0x1BC7D, 0x1BC7F}, {0x1BC89, 0x1BC8F}, {0x1BC9A, 0x1BC9B},
+	{0x1BCA4, 0x1CFFF}, {0x1D0F6, 0x1D0FF}, {0x1D127, 0x1D128},
+	{0x1D1E9, 0x1D1FF}, {0x1D246, 0x1D2FF}, {0x1D357, 0x1D35F},
+	{0x1D372, 0x1D3FF}, {0x1D455, 0x1D455}, {0x1D49D, 0x1D49D},
+	{0x1D4A0, 0x1D4A1}, {0x1D4A3, 0x1D4A4}, {0x1D4A7, 0x1D4A8},
+	{0x1D4AD, 0x1D4AD}, {0x1D4BA, 0x1D4BA}, {0x1D4BC, 0x1D4BC},
+	{0x1D4C4, 0x1D4C4}, {0x1D506, 0x1D506}, {0x1D50B, 0x1D50C},
+	{0x1D515, 0x1D515}, {0x1D51D, 0x1D51D}, {0x1D53A, 0x1D53A},
+	{0x1D53F, 0x1D53F}, {0x1D545, 0x1D545}, {0x1D547, 0x1D549},
+	{0x1D551, 0x1D551}, {0x1D6A6, 0x1D6A7}, {0x1D7CC, 0x1D7CD},
+	{0x1DA8C, 0x1DA9A}, {0x1DAA0, 0x1DAA0}, {0x1DAB0, 0x1DFFF},
+	{0x1E007, 0x1E007}, {0x1E019, 0x1E01A}, {0x1E022, 0x1E022},
+	{0x1E025, 0x1E025}, {0x1E02B, 0x1E7FF}, {0x1E8C5, 0x1E8C6},
+	{0x1E8D7, 0x1E8FF}, {0x1E94B, 0x1E94F}, {0x1E95A, 0x1E95D},
+	{0x1E960, 0x1EDFF}, {0x1EE04, 0x1EE04}, {0x1EE20, 0x1EE20},
+	{0x1EE23, 0x1EE23}, {0x1EE25, 0x1EE26}, {0x1EE28, 0x1EE28},
+	{0x1EE33, 0x1EE33}, {0x1EE38, 0x1EE38}, {0x1EE3A, 0x1EE3A},
+	{0x1EE3C, 0x1EE41}, {0x1EE43, 0x1EE46}, {0x1EE48, 0x1EE48},
+	{0x1EE4A, 0x1EE4A}, {0x1EE4C, 0x1EE4C}, {0x1EE50, 0x1EE50},
+	{0x1EE53, 0x1EE53}, {0x1EE55, 0x1EE56}, {0x1EE58, 0x1EE58},
+	{0x1EE5A, 0x1EE5A}, {0x1EE5C, 0x1EE5C}, {0x1EE5E, 0x1EE5E},
+	{0x1EE60, 0x1EE60}, {0x1EE63, 0x1EE63}, {0x1EE65, 0x1EE66},
+	{0x1EE6B, 0x1EE6B}, {0x1EE73, 0x1EE73}, {0x1EE78, 0x1EE78},
+	{0x1EE7D, 0x1EE7D}, {0x1EE7F, 0x1EE7F}, {0x1EE8A, 0x1EE8A},
+	{0x1EE9C, 0x1EEA0}, {0x1EEA4, 0x1EEA4}, {0x1EEAA, 0x1EEAA},
+	{0x1EEBC, 0x1EEEF}, {0x1EEF2, 0x1EFFF}, {0x1F02C, 0x1F02F},
+	{0x1F094, 0x1F09F}, {0x1F0AF, 0x1F0B0}, {0x1F0C0, 0x1F0C0},
+	{0x1F0D0, 0x1F0D0}, {0x1F0F6, 0x1F0FF}, {0x1F10D, 0x1F10F},
+	{0x1F12F, 0x1F12F}, {0x1F16C, 0x1F16F}, {0x1F1AD, 0x1F1E5},
+	{0x1F203, 0x1F20F}, {0x1F23C, 0x1F23F}, {0x1F249, 0x1F24F},
+	{0x1F252, 0x1F2FF}, {0x1F6D3, 0x1F6DF}, {0x1F6ED, 0x1F6EF},
+	{0x1F6F7, 0x1F6FF}, {0x1F774, 0x1F77F}, {0x1F7D5, 0x1F7FF},
+	{0x1F80C, 0x1F80F}, {0x1F848, 0x1F84F}, {0x1F85A, 0x1F85F},
+	{0x1F888, 0x1F88F}, {0x1F8AE, 0x1F90F}, {0x1F91F, 0x1F91F},
+	{0x1F928, 0x1F92F}, {0x1F931, 0x1F932}, {0x1F93F, 0x1F93F},
+	{0x1F94C, 0x1F94F}, {0x1F95F, 0x1F97F}, {0x1F992, 0x1F9BF},
+	{0x1F9C1, 0x1FFFF}, {0x2A6D7, 0x2A6FF}, {0x2B735, 0x2B73F},
+	{0x2B81E, 0x2B81F}, {0x2CEA2, 0x2F7FF}, {0x2FA1E, 0xE0000},
+	{0xE0002, 0xE001F}, {0xE0080, 0xE00FF}, {0xE01F0, 0xEFFFF},
+	{0xFFFFE, 0xFFFFF},
+}
+
+var neutral = table{
+	{0x0000, 0x001F}, {0x007F, 0x007F}, {0x0080, 0x009F},
+	{0x00A0, 0x00A0}, {0x00A9, 0x00A9}, {0x00AB, 0x00AB},
+	{0x00B5, 0x00B5}, {0x00BB, 0x00BB}, {0x00C0, 0x00C5},
+	{0x00C7, 0x00CF}, {0x00D1, 0x00D6}, {0x00D9, 0x00DD},
+	{0x00E2, 0x00E5}, {0x00E7, 0x00E7}, {0x00EB, 0x00EB},
+	{0x00EE, 0x00EF}, {0x00F1, 0x00F1}, {0x00F4, 0x00F6},
+	{0x00FB, 0x00FB}, {0x00FD, 0x00FD}, {0x00FF, 0x00FF},
+	{0x0100, 0x0100}, {0x0102, 0x0110}, {0x0112, 0x0112},
+	{0x0114, 0x011A}, {0x011C, 0x0125}, {0x0128, 0x012A},
+	{0x012C, 0x0130}, {0x0134, 0x0137}, {0x0139, 0x013E},
+	{0x0143, 0x0143}, {0x0145, 0x0147}, {0x014C, 0x014C},
+	{0x014E, 0x0151}, {0x0154, 0x0165}, {0x0168, 0x016A},
+	{0x016C, 0x017F}, {0x0180, 0x01BA}, {0x01BB, 0x01BB},
+	{0x01BC, 0x01BF}, {0x01C0, 0x01C3}, {0x01C4, 0x01CD},
+	{0x01CF, 0x01CF}, {0x01D1, 0x01D1}, {0x01D3, 0x01D3},
+	{0x01D5, 0x01D5}, {0x01D7, 0x01D7}, {0x01D9, 0x01D9},
+	{0x01DB, 0x01DB}, {0x01DD, 0x024F}, {0x0250, 0x0250},
+	{0x0252, 0x0260}, {0x0262, 0x0293}, {0x0294, 0x0294},
+	{0x0295, 0x02AF}, {0x02B0, 0x02C1}, {0x02C2, 0x02C3},
+	{0x02C5, 0x02C5}, {0x02C6, 0x02C6}, {0x02C8, 0x02C8},
+	{0x02CC, 0x02CC}, {0x02CE, 0x02CF}, {0x02D1, 0x02D1},
+	{0x02D2, 0x02D7}, {0x02DC, 0x02DC}, {0x02DE, 0x02DE},
+	{0x02E0, 0x02E4}, {0x02E5, 0x02EB}, {0x02EC, 0x02EC},
+	{0x02ED, 0x02ED}, {0x02EE, 0x02EE}, {0x02EF, 0x02FF},
+	{0x0370, 0x0373}, {0x0374, 0x0374}, {0x0375, 0x0375},
+	{0x0376, 0x0377}, {0x037A, 0x037A}, {0x037B, 0x037D},
+	{0x037E, 0x037E}, {0x037F, 0x037F}, {0x0384, 0x0385},
+	{0x0386, 0x0386}, {0x0387, 0x0387}, {0x0388, 0x038A},
+	{0x038C, 0x038C}, {0x038E, 0x0390}, {0x03AA, 0x03B0},
+	{0x03C2, 0x03C2}, {0x03CA, 0x03F5}, {0x03F6, 0x03F6},
+	{0x03F7, 0x03FF}, {0x0400, 0x0400}, {0x0402, 0x040F},
+	{0x0450, 0x0450}, {0x0452, 0x0481}, {0x0482, 0x0482},
+	{0x0483, 0x0487}, {0x0488, 0x0489}, {0x048A, 0x04FF},
+	{0x0500, 0x052F}, {0x0531, 0x0556}, {0x0559, 0x0559},
+	{0x055A, 0x055F}, {0x0561, 0x0587}, {0x0589, 0x0589},
+	{0x058A, 0x058A}, {0x058D, 0x058E}, {0x058F, 0x058F},
+	{0x0591, 0x05BD}, {0x05BE, 0x05BE}, {0x05BF, 0x05BF},
+	{0x05C0, 0x05C0}, {0x05C1, 0x05C2}, {0x05C3, 0x05C3},
+	{0x05C4, 0x05C5}, {0x05C6, 0x05C6}, {0x05C7, 0x05C7},
+	{0x05D0, 0x05EA}, {0x05F0, 0x05F2}, {0x05F3, 0x05F4},
+	{0x0600, 0x0605}, {0x0606, 0x0608}, {0x0609, 0x060A},
+	{0x060B, 0x060B}, {0x060C, 0x060D}, {0x060E, 0x060F},
+	{0x0610, 0x061A}, {0x061B, 0x061B}, {0x061C, 0x061C},
+	{0x061E, 0x061F}, {0x0620, 0x063F}, {0x0640, 0x0640},
+	{0x0641, 0x064A}, {0x064B, 0x065F}, {0x0660, 0x0669},
+	{0x066A, 0x066D}, {0x066E, 0x066F}, {0x0670, 0x0670},
+	{0x0671, 0x06D3}, {0x06D4, 0x06D4}, {0x06D5, 0x06D5},
+	{0x06D6, 0x06DC}, {0x06DD, 0x06DD}, {0x06DE, 0x06DE},
+	{0x06DF, 0x06E4}, {0x06E5, 0x06E6}, {0x06E7, 0x06E8},
+	{0x06E9, 0x06E9}, {0x06EA, 0x06ED}, {0x06EE, 0x06EF},
+	{0x06F0, 0x06F9}, {0x06FA, 0x06FC}, {0x06FD, 0x06FE},
+	{0x06FF, 0x06FF}, {0x0700, 0x070D}, {0x070F, 0x070F},
+	{0x0710, 0x0710}, {0x0711, 0x0711}, {0x0712, 0x072F},
+	{0x0730, 0x074A}, {0x074D, 0x074F}, {0x0750, 0x077F},
+	{0x0780, 0x07A5}, {0x07A6, 0x07B0}, {0x07B1, 0x07B1},
+	{0x07C0, 0x07C9}, {0x07CA, 0x07EA}, {0x07EB, 0x07F3},
+	{0x07F4, 0x07F5}, {0x07F6, 0x07F6}, {0x07F7, 0x07F9},
+	{0x07FA, 0x07FA}, {0x0800, 0x0815}, {0x0816, 0x0819},
+	{0x081A, 0x081A}, {0x081B, 0x0823}, {0x0824, 0x0824},
+	{0x0825, 0x0827}, {0x0828, 0x0828}, {0x0829, 0x082D},
+	{0x0830, 0x083E}, {0x0840, 0x0858}, {0x0859, 0x085B},
+	{0x085E, 0x085E}, {0x08A0, 0x08B4}, {0x08B6, 0x08BD},
+	{0x08D4, 0x08E1}, {0x08E2, 0x08E2}, {0x08E3, 0x08FF},
+	{0x0900, 0x0902}, {0x0903, 0x0903}, {0x0904, 0x0939},
+	{0x093A, 0x093A}, {0x093B, 0x093B}, {0x093C, 0x093C},
+	{0x093D, 0x093D}, {0x093E, 0x0940}, {0x0941, 0x0948},
+	{0x0949, 0x094C}, {0x094D, 0x094D}, {0x094E, 0x094F},
+	{0x0950, 0x0950}, {0x0951, 0x0957}, {0x0958, 0x0961},
+	{0x0962, 0x0963}, {0x0964, 0x0965}, {0x0966, 0x096F},
+	{0x0970, 0x0970}, {0x0971, 0x0971}, {0x0972, 0x097F},
+	{0x0980, 0x0980}, {0x0981, 0x0981}, {0x0982, 0x0983},
+	{0x0985, 0x098C}, {0x098F, 0x0990}, {0x0993, 0x09A8},
+	{0x09AA, 0x09B0}, {0x09B2, 0x09B2}, {0x09B6, 0x09B9},
+	{0x09BC, 0x09BC}, {0x09BD, 0x09BD}, {0x09BE, 0x09C0},
+	{0x09C1, 0x09C4}, {0x09C7, 0x09C8}, {0x09CB, 0x09CC},
+	{0x09CD, 0x09CD}, {0x09CE, 0x09CE}, {0x09D7, 0x09D7},
+	{0x09DC, 0x09DD}, {0x09DF, 0x09E1}, {0x09E2, 0x09E3},
+	{0x09E6, 0x09EF}, {0x09F0, 0x09F1}, {0x09F2, 0x09F3},
+	{0x09F4, 0x09F9}, {0x09FA, 0x09FA}, {0x09FB, 0x09FB},
+	{0x0A01, 0x0A02}, {0x0A03, 0x0A03}, {0x0A05, 0x0A0A},
+	{0x0A0F, 0x0A10}, {0x0A13, 0x0A28}, {0x0A2A, 0x0A30},
+	{0x0A32, 0x0A33}, {0x0A35, 0x0A36}, {0x0A38, 0x0A39},
+	{0x0A3C, 0x0A3C}, {0x0A3E, 0x0A40}, {0x0A41, 0x0A42},
+	{0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51},
+	{0x0A59, 0x0A5C}, {0x0A5E, 0x0A5E}, {0x0A66, 0x0A6F},
+	{0x0A70, 0x0A71}, {0x0A72, 0x0A74}, {0x0A75, 0x0A75},
+	{0x0A81, 0x0A82}, {0x0A83, 0x0A83}, {0x0A85, 0x0A8D},
+	{0x0A8F, 0x0A91}, {0x0A93, 0x0AA8}, {0x0AAA, 0x0AB0},
+	{0x0AB2, 0x0AB3}, {0x0AB5, 0x0AB9}, {0x0ABC, 0x0ABC},
+	{0x0ABD, 0x0ABD}, {0x0ABE, 0x0AC0}, {0x0AC1, 0x0AC5},
+	{0x0AC7, 0x0AC8}, {0x0AC9, 0x0AC9}, {0x0ACB, 0x0ACC},
+	{0x0ACD, 0x0ACD}, {0x0AD0, 0x0AD0}, {0x0AE0, 0x0AE1},
+	{0x0AE2, 0x0AE3}, {0x0AE6, 0x0AEF}, {0x0AF0, 0x0AF0},
+	{0x0AF1, 0x0AF1}, {0x0AF9, 0x0AF9}, {0x0B01, 0x0B01},
+	{0x0B02, 0x0B03}, {0x0B05, 0x0B0C}, {0x0B0F, 0x0B10},
+	{0x0B13, 0x0B28}, {0x0B2A, 0x0B30}, {0x0B32, 0x0B33},
+	{0x0B35, 0x0B39}, {0x0B3C, 0x0B3C}, {0x0B3D, 0x0B3D},
+	{0x0B3E, 0x0B3E}, {0x0B3F, 0x0B3F}, {0x0B40, 0x0B40},
+	{0x0B41, 0x0B44}, {0x0B47, 0x0B48}, {0x0B4B, 0x0B4C},
+	{0x0B4D, 0x0B4D}, {0x0B56, 0x0B56}, {0x0B57, 0x0B57},
+	{0x0B5C, 0x0B5D}, {0x0B5F, 0x0B61}, {0x0B62, 0x0B63},
+	{0x0B66, 0x0B6F}, {0x0B70, 0x0B70}, {0x0B71, 0x0B71},
+	{0x0B72, 0x0B77}, {0x0B82, 0x0B82}, {0x0B83, 0x0B83},
+	{0x0B85, 0x0B8A}, {0x0B8E, 0x0B90}, {0x0B92, 0x0B95},
+	{0x0B99, 0x0B9A}, {0x0B9C, 0x0B9C}, {0x0B9E, 0x0B9F},
+	{0x0BA3, 0x0BA4}, {0x0BA8, 0x0BAA}, {0x0BAE, 0x0BB9},
+	{0x0BBE, 0x0BBF}, {0x0BC0, 0x0BC0}, {0x0BC1, 0x0BC2},
+	{0x0BC6, 0x0BC8}, {0x0BCA, 0x0BCC}, {0x0BCD, 0x0BCD},
+	{0x0BD0, 0x0BD0}, {0x0BD7, 0x0BD7}, {0x0BE6, 0x0BEF},
+	{0x0BF0, 0x0BF2}, {0x0BF3, 0x0BF8}, {0x0BF9, 0x0BF9},
+	{0x0BFA, 0x0BFA}, {0x0C00, 0x0C00}, {0x0C01, 0x0C03},
+	{0x0C05, 0x0C0C}, {0x0C0E, 0x0C10}, {0x0C12, 0x0C28},
+	{0x0C2A, 0x0C39}, {0x0C3D, 0x0C3D}, {0x0C3E, 0x0C40},
+	{0x0C41, 0x0C44}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D},
+	{0x0C55, 0x0C56}, {0x0C58, 0x0C5A}, {0x0C60, 0x0C61},
+	{0x0C62, 0x0C63}, {0x0C66, 0x0C6F}, {0x0C78, 0x0C7E},
+	{0x0C7F, 0x0C7F}, {0x0C80, 0x0C80}, {0x0C81, 0x0C81},
+	{0x0C82, 0x0C83}, {0x0C85, 0x0C8C}, {0x0C8E, 0x0C90},
+	{0x0C92, 0x0CA8}, {0x0CAA, 0x0CB3}, {0x0CB5, 0x0CB9},
+	{0x0CBC, 0x0CBC}, {0x0CBD, 0x0CBD}, {0x0CBE, 0x0CBE},
+	{0x0CBF, 0x0CBF}, {0x0CC0, 0x0CC4}, {0x0CC6, 0x0CC6},
+	{0x0CC7, 0x0CC8}, {0x0CCA, 0x0CCB}, {0x0CCC, 0x0CCD},
+	{0x0CD5, 0x0CD6}, {0x0CDE, 0x0CDE}, {0x0CE0, 0x0CE1},
+	{0x0CE2, 0x0CE3}, {0x0CE6, 0x0CEF}, {0x0CF1, 0x0CF2},
+	{0x0D01, 0x0D01}, {0x0D02, 0x0D03}, {0x0D05, 0x0D0C},
+	{0x0D0E, 0x0D10}, {0x0D12, 0x0D3A}, {0x0D3D, 0x0D3D},
+	{0x0D3E, 0x0D40}, {0x0D41, 0x0D44}, {0x0D46, 0x0D48},
+	{0x0D4A, 0x0D4C}, {0x0D4D, 0x0D4D}, {0x0D4E, 0x0D4E},
+	{0x0D4F, 0x0D4F}, {0x0D54, 0x0D56}, {0x0D57, 0x0D57},
+	{0x0D58, 0x0D5E}, {0x0D5F, 0x0D61}, {0x0D62, 0x0D63},
+	{0x0D66, 0x0D6F}, {0x0D70, 0x0D78}, {0x0D79, 0x0D79},
+	{0x0D7A, 0x0D7F}, {0x0D82, 0x0D83}, {0x0D85, 0x0D96},
+	{0x0D9A, 0x0DB1}, {0x0DB3, 0x0DBB}, {0x0DBD, 0x0DBD},
+	{0x0DC0, 0x0DC6}, {0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD1},
+	{0x0DD2, 0x0DD4}, {0x0DD6, 0x0DD6}, {0x0DD8, 0x0DDF},
+	{0x0DE6, 0x0DEF}, {0x0DF2, 0x0DF3}, {0x0DF4, 0x0DF4},
+	{0x0E01, 0x0E30}, {0x0E31, 0x0E31}, {0x0E32, 0x0E33},
+	{0x0E34, 0x0E3A}, {0x0E3F, 0x0E3F}, {0x0E40, 0x0E45},
+	{0x0E46, 0x0E46}, {0x0E47, 0x0E4E}, {0x0E4F, 0x0E4F},
+	{0x0E50, 0x0E59}, {0x0E5A, 0x0E5B}, {0x0E81, 0x0E82},
+	{0x0E84, 0x0E84}, {0x0E87, 0x0E88}, {0x0E8A, 0x0E8A},
+	{0x0E8D, 0x0E8D}, {0x0E94, 0x0E97}, {0x0E99, 0x0E9F},
+	{0x0EA1, 0x0EA3}, {0x0EA5, 0x0EA5}, {0x0EA7, 0x0EA7},
+	{0x0EAA, 0x0EAB}, {0x0EAD, 0x0EB0}, {0x0EB1, 0x0EB1},
+	{0x0EB2, 0x0EB3}, {0x0EB4, 0x0EB9}, {0x0EBB, 0x0EBC},
+	{0x0EBD, 0x0EBD}, {0x0EC0, 0x0EC4}, {0x0EC6, 0x0EC6},
+	{0x0EC8, 0x0ECD}, {0x0ED0, 0x0ED9}, {0x0EDC, 0x0EDF},
+	{0x0F00, 0x0F00}, {0x0F01, 0x0F03}, {0x0F04, 0x0F12},
+	{0x0F13, 0x0F13}, {0x0F14, 0x0F14}, {0x0F15, 0x0F17},
+	{0x0F18, 0x0F19}, {0x0F1A, 0x0F1F}, {0x0F20, 0x0F29},
+	{0x0F2A, 0x0F33}, {0x0F34, 0x0F34}, {0x0F35, 0x0F35},
+	{0x0F36, 0x0F36}, {0x0F37, 0x0F37}, {0x0F38, 0x0F38},
+	{0x0F39, 0x0F39}, {0x0F3A, 0x0F3A}, {0x0F3B, 0x0F3B},
+	{0x0F3C, 0x0F3C}, {0x0F3D, 0x0F3D}, {0x0F3E, 0x0F3F},
+	{0x0F40, 0x0F47}, {0x0F49, 0x0F6C}, {0x0F71, 0x0F7E},
+	{0x0F7F, 0x0F7F}, {0x0F80, 0x0F84}, {0x0F85, 0x0F85},
+	{0x0F86, 0x0F87}, {0x0F88, 0x0F8C}, {0x0F8D, 0x0F97},
+	{0x0F99, 0x0FBC}, {0x0FBE, 0x0FC5}, {0x0FC6, 0x0FC6},
+	{0x0FC7, 0x0FCC}, {0x0FCE, 0x0FCF}, {0x0FD0, 0x0FD4},
+	{0x0FD5, 0x0FD8}, {0x0FD9, 0x0FDA}, {0x1000, 0x102A},
+	{0x102B, 0x102C}, {0x102D, 0x1030}, {0x1031, 0x1031},
+	{0x1032, 0x1037}, {0x1038, 0x1038}, {0x1039, 0x103A},
+	{0x103B, 0x103C}, {0x103D, 0x103E}, {0x103F, 0x103F},
+	{0x1040, 0x1049}, {0x104A, 0x104F}, {0x1050, 0x1055},
+	{0x1056, 0x1057}, {0x1058, 0x1059}, {0x105A, 0x105D},
+	{0x105E, 0x1060}, {0x1061, 0x1061}, {0x1062, 0x1064},
+	{0x1065, 0x1066}, {0x1067, 0x106D}, {0x106E, 0x1070},
+	{0x1071, 0x1074}, {0x1075, 0x1081}, {0x1082, 0x1082},
+	{0x1083, 0x1084}, {0x1085, 0x1086}, {0x1087, 0x108C},
+	{0x108D, 0x108D}, {0x108E, 0x108E}, {0x108F, 0x108F},
+	{0x1090, 0x1099}, {0x109A, 0x109C}, {0x109D, 0x109D},
+	{0x109E, 0x109F}, {0x10A0, 0x10C5}, {0x10C7, 0x10C7},
+	{0x10CD, 0x10CD}, {0x10D0, 0x10FA}, {0x10FB, 0x10FB},
+	{0x10FC, 0x10FC}, {0x10FD, 0x10FF}, {0x1160, 0x11FF},
+	{0x1200, 0x1248}, {0x124A, 0x124D}, {0x1250, 0x1256},
+	{0x1258, 0x1258}, {0x125A, 0x125D}, {0x1260, 0x1288},
+	{0x128A, 0x128D}, {0x1290, 0x12B0}, {0x12B2, 0x12B5},
+	{0x12B8, 0x12BE}, {0x12C0, 0x12C0}, {0x12C2, 0x12C5},
+	{0x12C8, 0x12D6}, {0x12D8, 0x1310}, {0x1312, 0x1315},
+	{0x1318, 0x135A}, {0x135D, 0x135F}, {0x1360, 0x1368},
+	{0x1369, 0x137C}, {0x1380, 0x138F}, {0x1390, 0x1399},
+	{0x13A0, 0x13F5}, {0x13F8, 0x13FD}, {0x1400, 0x1400},
+	{0x1401, 0x166C}, {0x166D, 0x166E}, {0x166F, 0x167F},
+	{0x1680, 0x1680}, {0x1681, 0x169A}, {0x169B, 0x169B},
+	{0x169C, 0x169C}, {0x16A0, 0x16EA}, {0x16EB, 0x16ED},
+	{0x16EE, 0x16F0}, {0x16F1, 0x16F8}, {0x1700, 0x170C},
+	{0x170E, 0x1711}, {0x1712, 0x1714}, {0x1720, 0x1731},
+	{0x1732, 0x1734}, {0x1735, 0x1736}, {0x1740, 0x1751},
+	{0x1752, 0x1753}, {0x1760, 0x176C}, {0x176E, 0x1770},
+	{0x1772, 0x1773}, {0x1780, 0x17B3}, {0x17B4, 0x17B5},
+	{0x17B6, 0x17B6}, {0x17B7, 0x17BD}, {0x17BE, 0x17C5},
+	{0x17C6, 0x17C6}, {0x17C7, 0x17C8}, {0x17C9, 0x17D3},
+	{0x17D4, 0x17D6}, {0x17D7, 0x17D7}, {0x17D8, 0x17DA},
+	{0x17DB, 0x17DB}, {0x17DC, 0x17DC}, {0x17DD, 0x17DD},
+	{0x17E0, 0x17E9}, {0x17F0, 0x17F9}, {0x1800, 0x1805},
+	{0x1806, 0x1806}, {0x1807, 0x180A}, {0x180B, 0x180D},
+	{0x180E, 0x180E}, {0x1810, 0x1819}, {0x1820, 0x1842},
+	{0x1843, 0x1843}, {0x1844, 0x1877}, {0x1880, 0x1884},
+	{0x1885, 0x1886}, {0x1887, 0x18A8}, {0x18A9, 0x18A9},
+	{0x18AA, 0x18AA}, {0x18B0, 0x18F5}, {0x1900, 0x191E},
+	{0x1920, 0x1922}, {0x1923, 0x1926}, {0x1927, 0x1928},
+	{0x1929, 0x192B}, {0x1930, 0x1931}, {0x1932, 0x1932},
+	{0x1933, 0x1938}, {0x1939, 0x193B}, {0x1940, 0x1940},
+	{0x1944, 0x1945}, {0x1946, 0x194F}, {0x1950, 0x196D},
+	{0x1970, 0x1974}, {0x1980, 0x19AB}, {0x19B0, 0x19C9},
+	{0x19D0, 0x19D9}, {0x19DA, 0x19DA}, {0x19DE, 0x19DF},
+	{0x19E0, 0x19FF}, {0x1A00, 0x1A16}, {0x1A17, 0x1A18},
+	{0x1A19, 0x1A1A}, {0x1A1B, 0x1A1B}, {0x1A1E, 0x1A1F},
+	{0x1A20, 0x1A54}, {0x1A55, 0x1A55}, {0x1A56, 0x1A56},
+	{0x1A57, 0x1A57}, {0x1A58, 0x1A5E}, {0x1A60, 0x1A60},
+	{0x1A61, 0x1A61}, {0x1A62, 0x1A62}, {0x1A63, 0x1A64},
+	{0x1A65, 0x1A6C}, {0x1A6D, 0x1A72}, {0x1A73, 0x1A7C},
+	{0x1A7F, 0x1A7F}, {0x1A80, 0x1A89}, {0x1A90, 0x1A99},
+	{0x1AA0, 0x1AA6}, {0x1AA7, 0x1AA7}, {0x1AA8, 0x1AAD},
+	{0x1AB0, 0x1ABD}, {0x1ABE, 0x1ABE}, {0x1B00, 0x1B03},
+	{0x1B04, 0x1B04}, {0x1B05, 0x1B33}, {0x1B34, 0x1B34},
+	{0x1B35, 0x1B35}, {0x1B36, 0x1B3A}, {0x1B3B, 0x1B3B},
+	{0x1B3C, 0x1B3C}, {0x1B3D, 0x1B41}, {0x1B42, 0x1B42},
+	{0x1B43, 0x1B44}, {0x1B45, 0x1B4B}, {0x1B50, 0x1B59},
+	{0x1B5A, 0x1B60}, {0x1B61, 0x1B6A}, {0x1B6B, 0x1B73},
+	{0x1B74, 0x1B7C}, {0x1B80, 0x1B81}, {0x1B82, 0x1B82},
+	{0x1B83, 0x1BA0}, {0x1BA1, 0x1BA1}, {0x1BA2, 0x1BA5},
+	{0x1BA6, 0x1BA7}, {0x1BA8, 0x1BA9}, {0x1BAA, 0x1BAA},
+	{0x1BAB, 0x1BAD}, {0x1BAE, 0x1BAF}, {0x1BB0, 0x1BB9},
+	{0x1BBA, 0x1BBF}, {0x1BC0, 0x1BE5}, {0x1BE6, 0x1BE6},
+	{0x1BE7, 0x1BE7}, {0x1BE8, 0x1BE9}, {0x1BEA, 0x1BEC},
+	{0x1BED, 0x1BED}, {0x1BEE, 0x1BEE}, {0x1BEF, 0x1BF1},
+	{0x1BF2, 0x1BF3}, {0x1BFC, 0x1BFF}, {0x1C00, 0x1C23},
+	{0x1C24, 0x1C2B}, {0x1C2C, 0x1C33}, {0x1C34, 0x1C35},
+	{0x1C36, 0x1C37}, {0x1C3B, 0x1C3F}, {0x1C40, 0x1C49},
+	{0x1C4D, 0x1C4F}, {0x1C50, 0x1C59}, {0x1C5A, 0x1C77},
+	{0x1C78, 0x1C7D}, {0x1C7E, 0x1C7F}, {0x1C80, 0x1C88},
+	{0x1CC0, 0x1CC7}, {0x1CD0, 0x1CD2}, {0x1CD3, 0x1CD3},
+	{0x1CD4, 0x1CE0}, {0x1CE1, 0x1CE1}, {0x1CE2, 0x1CE8},
+	{0x1CE9, 0x1CEC}, {0x1CED, 0x1CED}, {0x1CEE, 0x1CF1},
+	{0x1CF2, 0x1CF3}, {0x1CF4, 0x1CF4}, {0x1CF5, 0x1CF6},
+	{0x1CF8, 0x1CF9}, {0x1D00, 0x1D2B}, {0x1D2C, 0x1D6A},
+	{0x1D6B, 0x1D77}, {0x1D78, 0x1D78}, {0x1D79, 0x1D7F},
+	{0x1D80, 0x1D9A}, {0x1D9B, 0x1DBF}, {0x1DC0, 0x1DF5},
+	{0x1DFB, 0x1DFF}, {0x1E00, 0x1EFF}, {0x1F00, 0x1F15},
+	{0x1F18, 0x1F1D}, {0x1F20, 0x1F45}, {0x1F48, 0x1F4D},
+	{0x1F50, 0x1F57}, {0x1F59, 0x1F59}, {0x1F5B, 0x1F5B},
+	{0x1F5D, 0x1F5D}, {0x1F5F, 0x1F7D}, {0x1F80, 0x1FB4},
+	{0x1FB6, 0x1FBC}, {0x1FBD, 0x1FBD}, {0x1FBE, 0x1FBE},
+	{0x1FBF, 0x1FC1}, {0x1FC2, 0x1FC4}, {0x1FC6, 0x1FCC},
+	{0x1FCD, 0x1FCF}, {0x1FD0, 0x1FD3}, {0x1FD6, 0x1FDB},
+	{0x1FDD, 0x1FDF}, {0x1FE0, 0x1FEC}, {0x1FED, 0x1FEF},
+	{0x1FF2, 0x1FF4}, {0x1FF6, 0x1FFC}, {0x1FFD, 0x1FFE},
+	{0x2000, 0x200A}, {0x200B, 0x200F}, {0x2011, 0x2012},
+	{0x2017, 0x2017}, {0x201A, 0x201A}, {0x201B, 0x201B},
+	{0x201E, 0x201E}, {0x201F, 0x201F}, {0x2023, 0x2023},
+	{0x2028, 0x2028}, {0x2029, 0x2029}, {0x202A, 0x202E},
+	{0x202F, 0x202F}, {0x2031, 0x2031}, {0x2034, 0x2034},
+	{0x2036, 0x2038}, {0x2039, 0x2039}, {0x203A, 0x203A},
+	{0x203C, 0x203D}, {0x203F, 0x2040}, {0x2041, 0x2043},
+	{0x2044, 0x2044}, {0x2045, 0x2045}, {0x2046, 0x2046},
+	{0x2047, 0x2051}, {0x2052, 0x2052}, {0x2053, 0x2053},
+	{0x2054, 0x2054}, {0x2055, 0x205E}, {0x205F, 0x205F},
+	{0x2060, 0x2064}, {0x2066, 0x206F}, {0x2070, 0x2070},
+	{0x2071, 0x2071}, {0x2075, 0x2079}, {0x207A, 0x207C},
+	{0x207D, 0x207D}, {0x207E, 0x207E}, {0x2080, 0x2080},
+	{0x2085, 0x2089}, {0x208A, 0x208C}, {0x208D, 0x208D},
+	{0x208E, 0x208E}, {0x2090, 0x209C}, {0x20A0, 0x20A8},
+	{0x20AA, 0x20AB}, {0x20AD, 0x20BE}, {0x20D0, 0x20DC},
+	{0x20DD, 0x20E0}, {0x20E1, 0x20E1}, {0x20E2, 0x20E4},
+	{0x20E5, 0x20F0}, {0x2100, 0x2101}, {0x2102, 0x2102},
+	{0x2104, 0x2104}, {0x2106, 0x2106}, {0x2107, 0x2107},
+	{0x2108, 0x2108}, {0x210A, 0x2112}, {0x2114, 0x2114},
+	{0x2115, 0x2115}, {0x2117, 0x2117}, {0x2118, 0x2118},
+	{0x2119, 0x211D}, {0x211E, 0x2120}, {0x2123, 0x2123},
+	{0x2124, 0x2124}, {0x2125, 0x2125}, {0x2127, 0x2127},
+	{0x2128, 0x2128}, {0x2129, 0x2129}, {0x212A, 0x212A},
+	{0x212C, 0x212D}, {0x212E, 0x212E}, {0x212F, 0x2134},
+	{0x2135, 0x2138}, {0x2139, 0x2139}, {0x213A, 0x213B},
+	{0x213C, 0x213F}, {0x2140, 0x2144}, {0x2145, 0x2149},
+	{0x214A, 0x214A}, {0x214B, 0x214B}, {0x214C, 0x214D},
+	{0x214E, 0x214E}, {0x214F, 0x214F}, {0x2150, 0x2152},
+	{0x2155, 0x215A}, {0x215F, 0x215F}, {0x216C, 0x216F},
+	{0x217A, 0x2182}, {0x2183, 0x2184}, {0x2185, 0x2188},
+	{0x218A, 0x218B}, {0x219A, 0x219B}, {0x219C, 0x219F},
+	{0x21A0, 0x21A0}, {0x21A1, 0x21A2}, {0x21A3, 0x21A3},
+	{0x21A4, 0x21A5}, {0x21A6, 0x21A6}, {0x21A7, 0x21AD},
+	{0x21AE, 0x21AE}, {0x21AF, 0x21B7}, {0x21BA, 0x21CD},
+	{0x21CE, 0x21CF}, {0x21D0, 0x21D1}, {0x21D3, 0x21D3},
+	{0x21D5, 0x21E6}, {0x21E8, 0x21F3}, {0x21F4, 0x21FF},
+	{0x2201, 0x2201}, {0x2204, 0x2206}, {0x2209, 0x220A},
+	{0x220C, 0x220E}, {0x2210, 0x2210}, {0x2212, 0x2214},
+	{0x2216, 0x2219}, {0x221B, 0x221C}, {0x2221, 0x2222},
+	{0x2224, 0x2224}, {0x2226, 0x2226}, {0x222D, 0x222D},
+	{0x222F, 0x2233}, {0x2238, 0x223B}, {0x223E, 0x2247},
+	{0x2249, 0x224B}, {0x224D, 0x2251}, {0x2253, 0x225F},
+	{0x2262, 0x2263}, {0x2268, 0x2269}, {0x226C, 0x226D},
+	{0x2270, 0x2281}, {0x2284, 0x2285}, {0x2288, 0x2294},
+	{0x2296, 0x2298}, {0x229A, 0x22A4}, {0x22A6, 0x22BE},
+	{0x22C0, 0x22FF}, {0x2300, 0x2307}, {0x2308, 0x2308},
+	{0x2309, 0x2309}, {0x230A, 0x230A}, {0x230B, 0x230B},
+	{0x230C, 0x2311}, {0x2313, 0x2319}, {0x231C, 0x231F},
+	{0x2320, 0x2321}, {0x2322, 0x2328}, {0x232B, 0x237B},
+	{0x237C, 0x237C}, {0x237D, 0x239A}, {0x239B, 0x23B3},
+	{0x23B4, 0x23DB}, {0x23DC, 0x23E1}, {0x23E2, 0x23E8},
+	{0x23ED, 0x23EF}, {0x23F1, 0x23F2}, {0x23F4, 0x23FE},
+	{0x2400, 0x2426}, {0x2440, 0x244A}, {0x24EA, 0x24EA},
+	{0x254C, 0x254F}, {0x2574, 0x257F}, {0x2590, 0x2591},
+	{0x2596, 0x259F}, {0x25A2, 0x25A2}, {0x25AA, 0x25B1},
+	{0x25B4, 0x25B5}, {0x25B8, 0x25BB}, {0x25BE, 0x25BF},
+	{0x25C2, 0x25C5}, {0x25C9, 0x25CA}, {0x25CC, 0x25CD},
+	{0x25D2, 0x25E1}, {0x25E6, 0x25EE}, {0x25F0, 0x25F7},
+	{0x25F8, 0x25FC}, {0x25FF, 0x25FF}, {0x2600, 0x2604},
+	{0x2607, 0x2608}, {0x260A, 0x260D}, {0x2610, 0x2613},
+	{0x2616, 0x261B}, {0x261D, 0x261D}, {0x261F, 0x263F},
+	{0x2641, 0x2641}, {0x2643, 0x2647}, {0x2654, 0x265F},
+	{0x2662, 0x2662}, {0x2666, 0x2666}, {0x266B, 0x266B},
+	{0x266E, 0x266E}, {0x2670, 0x267E}, {0x2680, 0x2692},
+	{0x2694, 0x269D}, {0x26A0, 0x26A0}, {0x26A2, 0x26A9},
+	{0x26AC, 0x26BC}, {0x26C0, 0x26C3}, {0x26E2, 0x26E2},
+	{0x26E4, 0x26E7}, {0x2700, 0x2704}, {0x2706, 0x2709},
+	{0x270C, 0x2727}, {0x2729, 0x273C}, {0x273E, 0x274B},
+	{0x274D, 0x274D}, {0x274F, 0x2752}, {0x2756, 0x2756},
+	{0x2758, 0x2767}, {0x2768, 0x2768}, {0x2769, 0x2769},
+	{0x276A, 0x276A}, {0x276B, 0x276B}, {0x276C, 0x276C},
+	{0x276D, 0x276D}, {0x276E, 0x276E}, {0x276F, 0x276F},
+	{0x2770, 0x2770}, {0x2771, 0x2771}, {0x2772, 0x2772},
+	{0x2773, 0x2773}, {0x2774, 0x2774}, {0x2775, 0x2775},
+	{0x2780, 0x2793}, {0x2794, 0x2794}, {0x2798, 0x27AF},
+	{0x27B1, 0x27BE}, {0x27C0, 0x27C4}, {0x27C5, 0x27C5},
+	{0x27C6, 0x27C6}, {0x27C7, 0x27E5}, {0x27EE, 0x27EE},
+	{0x27EF, 0x27EF}, {0x27F0, 0x27FF}, {0x2800, 0x28FF},
+	{0x2900, 0x297F}, {0x2980, 0x2982}, {0x2983, 0x2983},
+	{0x2984, 0x2984}, {0x2987, 0x2987}, {0x2988, 0x2988},
+	{0x2989, 0x2989}, {0x298A, 0x298A}, {0x298B, 0x298B},
+	{0x298C, 0x298C}, {0x298D, 0x298D}, {0x298E, 0x298E},
+	{0x298F, 0x298F}, {0x2990, 0x2990}, {0x2991, 0x2991},
+	{0x2992, 0x2992}, {0x2993, 0x2993}, {0x2994, 0x2994},
+	{0x2995, 0x2995}, {0x2996, 0x2996}, {0x2997, 0x2997},
+	{0x2998, 0x2998}, {0x2999, 0x29D7}, {0x29D8, 0x29D8},
+	{0x29D9, 0x29D9}, {0x29DA, 0x29DA}, {0x29DB, 0x29DB},
+	{0x29DC, 0x29FB}, {0x29FC, 0x29FC}, {0x29FD, 0x29FD},
+	{0x29FE, 0x29FF}, {0x2A00, 0x2AFF}, {0x2B00, 0x2B1A},
+	{0x2B1D, 0x2B2F}, {0x2B30, 0x2B44}, {0x2B45, 0x2B46},
+	{0x2B47, 0x2B4C}, {0x2B4D, 0x2B4F}, {0x2B51, 0x2B54},
+	{0x2B5A, 0x2B73}, {0x2B76, 0x2B95}, {0x2B98, 0x2BB9},
+	{0x2BBD, 0x2BC8}, {0x2BCA, 0x2BD1}, {0x2BEC, 0x2BEF},
+	{0x2C00, 0x2C2E}, {0x2C30, 0x2C5E}, {0x2C60, 0x2C7B},
+	{0x2C7C, 0x2C7D}, {0x2C7E, 0x2C7F}, {0x2C80, 0x2CE4},
+	{0x2CE5, 0x2CEA}, {0x2CEB, 0x2CEE}, {0x2CEF, 0x2CF1},
+	{0x2CF2, 0x2CF3}, {0x2CF9, 0x2CFC}, {0x2CFD, 0x2CFD},
+	{0x2CFE, 0x2CFF}, {0x2D00, 0x2D25}, {0x2D27, 0x2D27},
+	{0x2D2D, 0x2D2D}, {0x2D30, 0x2D67}, {0x2D6F, 0x2D6F},
+	{0x2D70, 0x2D70}, {0x2D7F, 0x2D7F}, {0x2D80, 0x2D96},
+	{0x2DA0, 0x2DA6}, {0x2DA8, 0x2DAE}, {0x2DB0, 0x2DB6},
+	{0x2DB8, 0x2DBE}, {0x2DC0, 0x2DC6}, {0x2DC8, 0x2DCE},
+	{0x2DD0, 0x2DD6}, {0x2DD8, 0x2DDE}, {0x2DE0, 0x2DFF},
+	{0x2E00, 0x2E01}, {0x2E02, 0x2E02}, {0x2E03, 0x2E03},
+	{0x2E04, 0x2E04}, {0x2E05, 0x2E05}, {0x2E06, 0x2E08},
+	{0x2E09, 0x2E09}, {0x2E0A, 0x2E0A}, {0x2E0B, 0x2E0B},
+	{0x2E0C, 0x2E0C}, {0x2E0D, 0x2E0D}, {0x2E0E, 0x2E16},
+	{0x2E17, 0x2E17}, {0x2E18, 0x2E19}, {0x2E1A, 0x2E1A},
+	{0x2E1B, 0x2E1B}, {0x2E1C, 0x2E1C}, {0x2E1D, 0x2E1D},
+	{0x2E1E, 0x2E1F}, {0x2E20, 0x2E20}, {0x2E21, 0x2E21},
+	{0x2E22, 0x2E22}, {0x2E23, 0x2E23}, {0x2E24, 0x2E24},
+	{0x2E25, 0x2E25}, {0x2E26, 0x2E26}, {0x2E27, 0x2E27},
+	{0x2E28, 0x2E28}, {0x2E29, 0x2E29}, {0x2E2A, 0x2E2E},
+	{0x2E2F, 0x2E2F}, {0x2E30, 0x2E39}, {0x2E3A, 0x2E3B},
+	{0x2E3C, 0x2E3F}, {0x2E40, 0x2E40}, {0x2E41, 0x2E41},
+	{0x2E42, 0x2E42}, {0x2E43, 0x2E44}, {0x303F, 0x303F},
+	{0x4DC0, 0x4DFF}, {0xA4D0, 0xA4F7}, {0xA4F8, 0xA4FD},
+	{0xA4FE, 0xA4FF}, {0xA500, 0xA60B}, {0xA60C, 0xA60C},
+	{0xA60D, 0xA60F}, {0xA610, 0xA61F}, {0xA620, 0xA629},
+	{0xA62A, 0xA62B}, {0xA640, 0xA66D}, {0xA66E, 0xA66E},
+	{0xA66F, 0xA66F}, {0xA670, 0xA672}, {0xA673, 0xA673},
+	{0xA674, 0xA67D}, {0xA67E, 0xA67E}, {0xA67F, 0xA67F},
+	{0xA680, 0xA69B}, {0xA69C, 0xA69D}, {0xA69E, 0xA69F},
+	{0xA6A0, 0xA6E5}, {0xA6E6, 0xA6EF}, {0xA6F0, 0xA6F1},
+	{0xA6F2, 0xA6F7}, {0xA700, 0xA716}, {0xA717, 0xA71F},
+	{0xA720, 0xA721}, {0xA722, 0xA76F}, {0xA770, 0xA770},
+	{0xA771, 0xA787}, {0xA788, 0xA788}, {0xA789, 0xA78A},
+	{0xA78B, 0xA78E}, {0xA78F, 0xA78F}, {0xA790, 0xA7AE},
+	{0xA7B0, 0xA7B7}, {0xA7F7, 0xA7F7}, {0xA7F8, 0xA7F9},
+	{0xA7FA, 0xA7FA}, {0xA7FB, 0xA7FF}, {0xA800, 0xA801},
+	{0xA802, 0xA802}, {0xA803, 0xA805}, {0xA806, 0xA806},
+	{0xA807, 0xA80A}, {0xA80B, 0xA80B}, {0xA80C, 0xA822},
+	{0xA823, 0xA824}, {0xA825, 0xA826}, {0xA827, 0xA827},
+	{0xA828, 0xA82B}, {0xA830, 0xA835}, {0xA836, 0xA837},
+	{0xA838, 0xA838}, {0xA839, 0xA839}, {0xA840, 0xA873},
+	{0xA874, 0xA877}, {0xA880, 0xA881}, {0xA882, 0xA8B3},
+	{0xA8B4, 0xA8C3}, {0xA8C4, 0xA8C5}, {0xA8CE, 0xA8CF},
+	{0xA8D0, 0xA8D9}, {0xA8E0, 0xA8F1}, {0xA8F2, 0xA8F7},
+	{0xA8F8, 0xA8FA}, {0xA8FB, 0xA8FB}, {0xA8FC, 0xA8FC},
+	{0xA8FD, 0xA8FD}, {0xA900, 0xA909}, {0xA90A, 0xA925},
+	{0xA926, 0xA92D}, {0xA92E, 0xA92F}, {0xA930, 0xA946},
+	{0xA947, 0xA951}, {0xA952, 0xA953}, {0xA95F, 0xA95F},
+	{0xA980, 0xA982}, {0xA983, 0xA983}, {0xA984, 0xA9B2},
+	{0xA9B3, 0xA9B3}, {0xA9B4, 0xA9B5}, {0xA9B6, 0xA9B9},
+	{0xA9BA, 0xA9BB}, {0xA9BC, 0xA9BC}, {0xA9BD, 0xA9C0},
+	{0xA9C1, 0xA9CD}, {0xA9CF, 0xA9CF}, {0xA9D0, 0xA9D9},
+	{0xA9DE, 0xA9DF}, {0xA9E0, 0xA9E4}, {0xA9E5, 0xA9E5},
+	{0xA9E6, 0xA9E6}, {0xA9E7, 0xA9EF}, {0xA9F0, 0xA9F9},
+	{0xA9FA, 0xA9FE}, {0xAA00, 0xAA28}, {0xAA29, 0xAA2E},
+	{0xAA2F, 0xAA30}, {0xAA31, 0xAA32}, {0xAA33, 0xAA34},
+	{0xAA35, 0xAA36}, {0xAA40, 0xAA42}, {0xAA43, 0xAA43},
+	{0xAA44, 0xAA4B}, {0xAA4C, 0xAA4C}, {0xAA4D, 0xAA4D},
+	{0xAA50, 0xAA59}, {0xAA5C, 0xAA5F}, {0xAA60, 0xAA6F},
+	{0xAA70, 0xAA70}, {0xAA71, 0xAA76}, {0xAA77, 0xAA79},
+	{0xAA7A, 0xAA7A}, {0xAA7B, 0xAA7B}, {0xAA7C, 0xAA7C},
+	{0xAA7D, 0xAA7D}, {0xAA7E, 0xAA7F}, {0xAA80, 0xAAAF},
+	{0xAAB0, 0xAAB0}, {0xAAB1, 0xAAB1}, {0xAAB2, 0xAAB4},
+	{0xAAB5, 0xAAB6}, {0xAAB7, 0xAAB8}, {0xAAB9, 0xAABD},
+	{0xAABE, 0xAABF}, {0xAAC0, 0xAAC0}, {0xAAC1, 0xAAC1},
+	{0xAAC2, 0xAAC2}, {0xAADB, 0xAADC}, {0xAADD, 0xAADD},
+	{0xAADE, 0xAADF}, {0xAAE0, 0xAAEA}, {0xAAEB, 0xAAEB},
+	{0xAAEC, 0xAAED}, {0xAAEE, 0xAAEF}, {0xAAF0, 0xAAF1},
+	{0xAAF2, 0xAAF2}, {0xAAF3, 0xAAF4}, {0xAAF5, 0xAAF5},
+	{0xAAF6, 0xAAF6}, {0xAB01, 0xAB06}, {0xAB09, 0xAB0E},
+	{0xAB11, 0xAB16}, {0xAB20, 0xAB26}, {0xAB28, 0xAB2E},
+	{0xAB30, 0xAB5A}, {0xAB5B, 0xAB5B}, {0xAB5C, 0xAB5F},
+	{0xAB60, 0xAB65}, {0xAB70, 0xABBF}, {0xABC0, 0xABE2},
+	{0xABE3, 0xABE4}, {0xABE5, 0xABE5}, {0xABE6, 0xABE7},
+	{0xABE8, 0xABE8}, {0xABE9, 0xABEA}, {0xABEB, 0xABEB},
+	{0xABEC, 0xABEC}, {0xABED, 0xABED}, {0xABF0, 0xABF9},
+	{0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB}, {0xD800, 0xDB7F},
+	{0xDB80, 0xDBFF}, {0xDC00, 0xDFFF}, {0xFB00, 0xFB06},
+	{0xFB13, 0xFB17}, {0xFB1D, 0xFB1D}, {0xFB1E, 0xFB1E},
+	{0xFB1F, 0xFB28}, {0xFB29, 0xFB29}, {0xFB2A, 0xFB36},
+	{0xFB38, 0xFB3C}, {0xFB3E, 0xFB3E}, {0xFB40, 0xFB41},
+	{0xFB43, 0xFB44}, {0xFB46, 0xFB4F}, {0xFB50, 0xFBB1},
+	{0xFBB2, 0xFBC1}, {0xFBD3, 0xFD3D}, {0xFD3E, 0xFD3E},
+	{0xFD3F, 0xFD3F}, {0xFD50, 0xFD8F}, {0xFD92, 0xFDC7},
+	{0xFDF0, 0xFDFB}, {0xFDFC, 0xFDFC}, {0xFDFD, 0xFDFD},
+	{0xFE20, 0xFE2F}, {0xFE70, 0xFE74}, {0xFE76, 0xFEFC},
+	{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFC, 0xFFFC},
+	{0x10000, 0x1000B}, {0x1000D, 0x10026}, {0x10028, 0x1003A},
+	{0x1003C, 0x1003D}, {0x1003F, 0x1004D}, {0x10050, 0x1005D},
+	{0x10080, 0x100FA}, {0x10100, 0x10102}, {0x10107, 0x10133},
+	{0x10137, 0x1013F}, {0x10140, 0x10174}, {0x10175, 0x10178},
+	{0x10179, 0x10189}, {0x1018A, 0x1018B}, {0x1018C, 0x1018E},
+	{0x10190, 0x1019B}, {0x101A0, 0x101A0}, {0x101D0, 0x101FC},
+	{0x101FD, 0x101FD}, {0x10280, 0x1029C}, {0x102A0, 0x102D0},
+	{0x102E0, 0x102E0}, {0x102E1, 0x102FB}, {0x10300, 0x1031F},
+	{0x10320, 0x10323}, {0x10330, 0x10340}, {0x10341, 0x10341},
+	{0x10342, 0x10349}, {0x1034A, 0x1034A}, {0x10350, 0x10375},
+	{0x10376, 0x1037A}, {0x10380, 0x1039D}, {0x1039F, 0x1039F},
+	{0x103A0, 0x103C3}, {0x103C8, 0x103CF}, {0x103D0, 0x103D0},
+	{0x103D1, 0x103D5}, {0x10400, 0x1044F}, {0x10450, 0x1047F},
+	{0x10480, 0x1049D}, {0x104A0, 0x104A9}, {0x104B0, 0x104D3},
+	{0x104D8, 0x104FB}, {0x10500, 0x10527}, {0x10530, 0x10563},
+	{0x1056F, 0x1056F}, {0x10600, 0x10736}, {0x10740, 0x10755},
+	{0x10760, 0x10767}, {0x10800, 0x10805}, {0x10808, 0x10808},
+	{0x1080A, 0x10835}, {0x10837, 0x10838}, {0x1083C, 0x1083C},
+	{0x1083F, 0x1083F}, {0x10840, 0x10855}, {0x10857, 0x10857},
+	{0x10858, 0x1085F}, {0x10860, 0x10876}, {0x10877, 0x10878},
+	{0x10879, 0x1087F}, {0x10880, 0x1089E}, {0x108A7, 0x108AF},
+	{0x108E0, 0x108F2}, {0x108F4, 0x108F5}, {0x108FB, 0x108FF},
+	{0x10900, 0x10915}, {0x10916, 0x1091B}, {0x1091F, 0x1091F},
+	{0x10920, 0x10939}, {0x1093F, 0x1093F}, {0x10980, 0x1099F},
+	{0x109A0, 0x109B7}, {0x109BC, 0x109BD}, {0x109BE, 0x109BF},
+	{0x109C0, 0x109CF}, {0x109D2, 0x109FF}, {0x10A00, 0x10A00},
+	{0x10A01, 0x10A03}, {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F},
+	{0x10A10, 0x10A13}, {0x10A15, 0x10A17}, {0x10A19, 0x10A33},
+	{0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F}, {0x10A40, 0x10A47},
+	{0x10A50, 0x10A58}, {0x10A60, 0x10A7C}, {0x10A7D, 0x10A7E},
+	{0x10A7F, 0x10A7F}, {0x10A80, 0x10A9C}, {0x10A9D, 0x10A9F},
+	{0x10AC0, 0x10AC7}, {0x10AC8, 0x10AC8}, {0x10AC9, 0x10AE4},
+	{0x10AE5, 0x10AE6}, {0x10AEB, 0x10AEF}, {0x10AF0, 0x10AF6},
+	{0x10B00, 0x10B35}, {0x10B39, 0x10B3F}, {0x10B40, 0x10B55},
+	{0x10B58, 0x10B5F}, {0x10B60, 0x10B72}, {0x10B78, 0x10B7F},
+	{0x10B80, 0x10B91}, {0x10B99, 0x10B9C}, {0x10BA9, 0x10BAF},
+	{0x10C00, 0x10C48}, {0x10C80, 0x10CB2}, {0x10CC0, 0x10CF2},
+	{0x10CFA, 0x10CFF}, {0x10E60, 0x10E7E}, {0x11000, 0x11000},
+	{0x11001, 0x11001}, {0x11002, 0x11002}, {0x11003, 0x11037},
+	{0x11038, 0x11046}, {0x11047, 0x1104D}, {0x11052, 0x11065},
+	{0x11066, 0x1106F}, {0x1107F, 0x1107F}, {0x11080, 0x11081},
+	{0x11082, 0x11082}, {0x11083, 0x110AF}, {0x110B0, 0x110B2},
+	{0x110B3, 0x110B6}, {0x110B7, 0x110B8}, {0x110B9, 0x110BA},
+	{0x110BB, 0x110BC}, {0x110BD, 0x110BD}, {0x110BE, 0x110C1},
+	{0x110D0, 0x110E8}, {0x110F0, 0x110F9}, {0x11100, 0x11102},
+	{0x11103, 0x11126}, {0x11127, 0x1112B}, {0x1112C, 0x1112C},
+	{0x1112D, 0x11134}, {0x11136, 0x1113F}, {0x11140, 0x11143},
+	{0x11150, 0x11172}, {0x11173, 0x11173}, {0x11174, 0x11175},
+	{0x11176, 0x11176}, {0x11180, 0x11181}, {0x11182, 0x11182},
+	{0x11183, 0x111B2}, {0x111B3, 0x111B5}, {0x111B6, 0x111BE},
+	{0x111BF, 0x111C0}, {0x111C1, 0x111C4}, {0x111C5, 0x111C9},
+	{0x111CA, 0x111CC}, {0x111CD, 0x111CD}, {0x111D0, 0x111D9},
+	{0x111DA, 0x111DA}, {0x111DB, 0x111DB}, {0x111DC, 0x111DC},
+	{0x111DD, 0x111DF}, {0x111E1, 0x111F4}, {0x11200, 0x11211},
+	{0x11213, 0x1122B}, {0x1122C, 0x1122E}, {0x1122F, 0x11231},
+	{0x11232, 0x11233}, {0x11234, 0x11234}, {0x11235, 0x11235},
+	{0x11236, 0x11237}, {0x11238, 0x1123D}, {0x1123E, 0x1123E},
+	{0x11280, 0x11286}, {0x11288, 0x11288}, {0x1128A, 0x1128D},
+	{0x1128F, 0x1129D}, {0x1129F, 0x112A8}, {0x112A9, 0x112A9},
+	{0x112B0, 0x112DE}, {0x112DF, 0x112DF}, {0x112E0, 0x112E2},
+	{0x112E3, 0x112EA}, {0x112F0, 0x112F9}, {0x11300, 0x11301},
+	{0x11302, 0x11303}, {0x11305, 0x1130C}, {0x1130F, 0x11310},
+	{0x11313, 0x11328}, {0x1132A, 0x11330}, {0x11332, 0x11333},
+	{0x11335, 0x11339}, {0x1133C, 0x1133C}, {0x1133D, 0x1133D},
+	{0x1133E, 0x1133F}, {0x11340, 0x11340}, {0x11341, 0x11344},
+	{0x11347, 0x11348}, {0x1134B, 0x1134D}, {0x11350, 0x11350},
+	{0x11357, 0x11357}, {0x1135D, 0x11361}, {0x11362, 0x11363},
+	{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x11400, 0x11434},
+	{0x11435, 0x11437}, {0x11438, 0x1143F}, {0x11440, 0x11441},
+	{0x11442, 0x11444}, {0x11445, 0x11445}, {0x11446, 0x11446},
+	{0x11447, 0x1144A}, {0x1144B, 0x1144F}, {0x11450, 0x11459},
+	{0x1145B, 0x1145B}, {0x1145D, 0x1145D}, {0x11480, 0x114AF},
+	{0x114B0, 0x114B2}, {0x114B3, 0x114B8}, {0x114B9, 0x114B9},
+	{0x114BA, 0x114BA}, {0x114BB, 0x114BE}, {0x114BF, 0x114C0},
+	{0x114C1, 0x114C1}, {0x114C2, 0x114C3}, {0x114C4, 0x114C5},
+	{0x114C6, 0x114C6}, {0x114C7, 0x114C7}, {0x114D0, 0x114D9},
+	{0x11580, 0x115AE}, {0x115AF, 0x115B1}, {0x115B2, 0x115B5},
+	{0x115B8, 0x115BB}, {0x115BC, 0x115BD}, {0x115BE, 0x115BE},
+	{0x115BF, 0x115C0}, {0x115C1, 0x115D7}, {0x115D8, 0x115DB},
+	{0x115DC, 0x115DD}, {0x11600, 0x1162F}, {0x11630, 0x11632},
+	{0x11633, 0x1163A}, {0x1163B, 0x1163C}, {0x1163D, 0x1163D},
+	{0x1163E, 0x1163E}, {0x1163F, 0x11640}, {0x11641, 0x11643},
+	{0x11644, 0x11644}, {0x11650, 0x11659}, {0x11660, 0x1166C},
+	{0x11680, 0x116AA}, {0x116AB, 0x116AB}, {0x116AC, 0x116AC},
+	{0x116AD, 0x116AD}, {0x116AE, 0x116AF}, {0x116B0, 0x116B5},
+	{0x116B6, 0x116B6}, {0x116B7, 0x116B7}, {0x116C0, 0x116C9},
+	{0x11700, 0x11719}, {0x1171D, 0x1171F}, {0x11720, 0x11721},
+	{0x11722, 0x11725}, {0x11726, 0x11726}, {0x11727, 0x1172B},
+	{0x11730, 0x11739}, {0x1173A, 0x1173B}, {0x1173C, 0x1173E},
+	{0x1173F, 0x1173F}, {0x118A0, 0x118DF}, {0x118E0, 0x118E9},
+	{0x118EA, 0x118F2}, {0x118FF, 0x118FF}, {0x11AC0, 0x11AF8},
+	{0x11C00, 0x11C08}, {0x11C0A, 0x11C2E}, {0x11C2F, 0x11C2F},
+	{0x11C30, 0x11C36}, {0x11C38, 0x11C3D}, {0x11C3E, 0x11C3E},
+	{0x11C3F, 0x11C3F}, {0x11C40, 0x11C40}, {0x11C41, 0x11C45},
+	{0x11C50, 0x11C59}, {0x11C5A, 0x11C6C}, {0x11C70, 0x11C71},
+	{0x11C72, 0x11C8F}, {0x11C92, 0x11CA7}, {0x11CA9, 0x11CA9},
+	{0x11CAA, 0x11CB0}, {0x11CB1, 0x11CB1}, {0x11CB2, 0x11CB3},
+	{0x11CB4, 0x11CB4}, {0x11CB5, 0x11CB6}, {0x12000, 0x12399},
+	{0x12400, 0x1246E}, {0x12470, 0x12474}, {0x12480, 0x12543},
+	{0x13000, 0x1342E}, {0x14400, 0x14646}, {0x16800, 0x16A38},
+	{0x16A40, 0x16A5E}, {0x16A60, 0x16A69}, {0x16A6E, 0x16A6F},
+	{0x16AD0, 0x16AED}, {0x16AF0, 0x16AF4}, {0x16AF5, 0x16AF5},
+	{0x16B00, 0x16B2F}, {0x16B30, 0x16B36}, {0x16B37, 0x16B3B},
+	{0x16B3C, 0x16B3F}, {0x16B40, 0x16B43}, {0x16B44, 0x16B44},
+	{0x16B45, 0x16B45}, {0x16B50, 0x16B59}, {0x16B5B, 0x16B61},
+	{0x16B63, 0x16B77}, {0x16B7D, 0x16B8F}, {0x16F00, 0x16F44},
+	{0x16F50, 0x16F50}, {0x16F51, 0x16F7E}, {0x16F8F, 0x16F92},
+	{0x16F93, 0x16F9F}, {0x1BC00, 0x1BC6A}, {0x1BC70, 0x1BC7C},
+	{0x1BC80, 0x1BC88}, {0x1BC90, 0x1BC99}, {0x1BC9C, 0x1BC9C},
+	{0x1BC9D, 0x1BC9E}, {0x1BC9F, 0x1BC9F}, {0x1BCA0, 0x1BCA3},
+	{0x1D000, 0x1D0F5}, {0x1D100, 0x1D126}, {0x1D129, 0x1D164},
+	{0x1D165, 0x1D166}, {0x1D167, 0x1D169}, {0x1D16A, 0x1D16C},
+	{0x1D16D, 0x1D172}, {0x1D173, 0x1D17A}, {0x1D17B, 0x1D182},
+	{0x1D183, 0x1D184}, {0x1D185, 0x1D18B}, {0x1D18C, 0x1D1A9},
+	{0x1D1AA, 0x1D1AD}, {0x1D1AE, 0x1D1E8}, {0x1D200, 0x1D241},
+	{0x1D242, 0x1D244}, {0x1D245, 0x1D245}, {0x1D300, 0x1D356},
+	{0x1D360, 0x1D371}, {0x1D400, 0x1D454}, {0x1D456, 0x1D49C},
+	{0x1D49E, 0x1D49F}, {0x1D4A2, 0x1D4A2}, {0x1D4A5, 0x1D4A6},
+	{0x1D4A9, 0x1D4AC}, {0x1D4AE, 0x1D4B9}, {0x1D4BB, 0x1D4BB},
+	{0x1D4BD, 0x1D4C3}, {0x1D4C5, 0x1D505}, {0x1D507, 0x1D50A},
+	{0x1D50D, 0x1D514}, {0x1D516, 0x1D51C}, {0x1D51E, 0x1D539},
+	{0x1D53B, 0x1D53E}, {0x1D540, 0x1D544}, {0x1D546, 0x1D546},
+	{0x1D54A, 0x1D550}, {0x1D552, 0x1D6A5}, {0x1D6A8, 0x1D6C0},
+	{0x1D6C1, 0x1D6C1}, {0x1D6C2, 0x1D6DA}, {0x1D6DB, 0x1D6DB},
+	{0x1D6DC, 0x1D6FA}, {0x1D6FB, 0x1D6FB}, {0x1D6FC, 0x1D714},
+	{0x1D715, 0x1D715}, {0x1D716, 0x1D734}, {0x1D735, 0x1D735},
+	{0x1D736, 0x1D74E}, {0x1D74F, 0x1D74F}, {0x1D750, 0x1D76E},
+	{0x1D76F, 0x1D76F}, {0x1D770, 0x1D788}, {0x1D789, 0x1D789},
+	{0x1D78A, 0x1D7A8}, {0x1D7A9, 0x1D7A9}, {0x1D7AA, 0x1D7C2},
+	{0x1D7C3, 0x1D7C3}, {0x1D7C4, 0x1D7CB}, {0x1D7CE, 0x1D7FF},
+	{0x1D800, 0x1D9FF}, {0x1DA00, 0x1DA36}, {0x1DA37, 0x1DA3A},
+	{0x1DA3B, 0x1DA6C}, {0x1DA6D, 0x1DA74}, {0x1DA75, 0x1DA75},
+	{0x1DA76, 0x1DA83}, {0x1DA84, 0x1DA84}, {0x1DA85, 0x1DA86},
+	{0x1DA87, 0x1DA8B}, {0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF},
+	{0x1E000, 0x1E006}, {0x1E008, 0x1E018}, {0x1E01B, 0x1E021},
+	{0x1E023, 0x1E024}, {0x1E026, 0x1E02A}, {0x1E800, 0x1E8C4},
+	{0x1E8C7, 0x1E8CF}, {0x1E8D0, 0x1E8D6}, {0x1E900, 0x1E943},
+	{0x1E944, 0x1E94A}, {0x1E950, 0x1E959}, {0x1E95E, 0x1E95F},
+	{0x1EE00, 0x1EE03}, {0x1EE05, 0x1EE1F}, {0x1EE21, 0x1EE22},
+	{0x1EE24, 0x1EE24}, {0x1EE27, 0x1EE27}, {0x1EE29, 0x1EE32},
+	{0x1EE34, 0x1EE37}, {0x1EE39, 0x1EE39}, {0x1EE3B, 0x1EE3B},
+	{0x1EE42, 0x1EE42}, {0x1EE47, 0x1EE47}, {0x1EE49, 0x1EE49},
+	{0x1EE4B, 0x1EE4B}, {0x1EE4D, 0x1EE4F}, {0x1EE51, 0x1EE52},
+	{0x1EE54, 0x1EE54}, {0x1EE57, 0x1EE57}, {0x1EE59, 0x1EE59},
+	{0x1EE5B, 0x1EE5B}, {0x1EE5D, 0x1EE5D}, {0x1EE5F, 0x1EE5F},
+	{0x1EE61, 0x1EE62}, {0x1EE64, 0x1EE64}, {0x1EE67, 0x1EE6A},
+	{0x1EE6C, 0x1EE72}, {0x1EE74, 0x1EE77}, {0x1EE79, 0x1EE7C},
+	{0x1EE7E, 0x1EE7E}, {0x1EE80, 0x1EE89}, {0x1EE8B, 0x1EE9B},
+	{0x1EEA1, 0x1EEA3}, {0x1EEA5, 0x1EEA9}, {0x1EEAB, 0x1EEBB},
+	{0x1EEF0, 0x1EEF1}, {0x1F000, 0x1F003}, {0x1F005, 0x1F02B},
+	{0x1F030, 0x1F093}, {0x1F0A0, 0x1F0AE}, {0x1F0B1, 0x1F0BF},
+	{0x1F0C1, 0x1F0CE}, {0x1F0D1, 0x1F0F5}, {0x1F10B, 0x1F10C},
+	{0x1F12E, 0x1F12E}, {0x1F16A, 0x1F16B}, {0x1F1E6, 0x1F1FF},
+	{0x1F321, 0x1F32C}, {0x1F336, 0x1F336}, {0x1F37D, 0x1F37D},
+	{0x1F394, 0x1F39F}, {0x1F3CB, 0x1F3CE}, {0x1F3D4, 0x1F3DF},
+	{0x1F3F1, 0x1F3F3}, {0x1F3F5, 0x1F3F7}, {0x1F43F, 0x1F43F},
+	{0x1F441, 0x1F441}, {0x1F4FD, 0x1F4FE}, {0x1F53E, 0x1F54A},
+	{0x1F54F, 0x1F54F}, {0x1F568, 0x1F579}, {0x1F57B, 0x1F594},
+	{0x1F597, 0x1F5A3}, {0x1F5A5, 0x1F5FA}, {0x1F650, 0x1F67F},
+	{0x1F6C6, 0x1F6CB}, {0x1F6CD, 0x1F6CF}, {0x1F6E0, 0x1F6EA},
+	{0x1F6F0, 0x1F6F3}, {0x1F700, 0x1F773}, {0x1F780, 0x1F7D4},
+	{0x1F800, 0x1F80B}, {0x1F810, 0x1F847}, {0x1F850, 0x1F859},
+	{0x1F860, 0x1F887}, {0x1F890, 0x1F8AD}, {0xE0001, 0xE0001},
+	{0xE0020, 0xE007F},
+}
+
+// Condition have flag EastAsianWidth whether the current locale is CJK or not.
+type Condition struct {
+	EastAsianWidth bool
+}
+
+// NewCondition return new instance of Condition which is current locale.
+func NewCondition() *Condition {
+	return &Condition{EastAsianWidth}
+}
+
+// RuneWidth returns the number of cells in r.
+// See http://www.unicode.org/reports/tr11/
+func (c *Condition) RuneWidth(r rune) int {
+	switch {
+	case r < 0 || r > 0x10FFFF ||
+		inTables(r, nonprint, combining, notassigned):
+		return 0
+	case (c.EastAsianWidth && IsAmbiguousWidth(r)) ||
+		inTables(r, doublewidth, emoji):
+		return 2
+	default:
+		return 1
+	}
+}
+
+// StringWidth return width as you can see
+func (c *Condition) StringWidth(s string) (width int) {
+	for _, r := range []rune(s) {
+		width += c.RuneWidth(r)
+	}
+	return width
+}
+
+// Truncate return string truncated with w cells
+func (c *Condition) Truncate(s string, w int, tail string) string {
+	if c.StringWidth(s) <= w {
+		return s
+	}
+	r := []rune(s)
+	tw := c.StringWidth(tail)
+	w -= tw
+	width := 0
+	i := 0
+	for ; i < len(r); i++ {
+		cw := c.RuneWidth(r[i])
+		if width+cw > w {
+			break
+		}
+		width += cw
+	}
+	return string(r[0:i]) + tail
+}
+
+// Wrap return string wrapped with w cells
+func (c *Condition) Wrap(s string, w int) string {
+	width := 0
+	out := ""
+	for _, r := range []rune(s) {
+		cw := RuneWidth(r)
+		if r == '\n' {
+			out += string(r)
+			width = 0
+			continue
+		} else if width+cw > w {
+			out += "\n"
+			width = 0
+			out += string(r)
+			width += cw
+			continue
+		}
+		out += string(r)
+		width += cw
+	}
+	return out
+}
+
+// FillLeft return string filled in left by spaces in w cells
+func (c *Condition) FillLeft(s string, w int) string {
+	width := c.StringWidth(s)
+	count := w - width
+	if count > 0 {
+		b := make([]byte, count)
+		for i := range b {
+			b[i] = ' '
+		}
+		return string(b) + s
+	}
+	return s
+}
+
+// FillRight return string filled in left by spaces in w cells
+func (c *Condition) FillRight(s string, w int) string {
+	width := c.StringWidth(s)
+	count := w - width
+	if count > 0 {
+		b := make([]byte, count)
+		for i := range b {
+			b[i] = ' '
+		}
+		return s + string(b)
+	}
+	return s
+}
+
+// RuneWidth returns the number of cells in r.
+// See http://www.unicode.org/reports/tr11/
+func RuneWidth(r rune) int {
+	return DefaultCondition.RuneWidth(r)
+}
+
+// IsAmbiguousWidth returns whether is ambiguous width or not.
+func IsAmbiguousWidth(r rune) bool {
+	return inTables(r, private, ambiguous)
+}
+
+// IsNeutralWidth returns whether is neutral width or not.
+func IsNeutralWidth(r rune) bool {
+	return inTable(r, neutral)
+}
+
+// StringWidth return width as you can see
+func StringWidth(s string) (width int) {
+	return DefaultCondition.StringWidth(s)
+}
+
+// Truncate return string truncated with w cells
+func Truncate(s string, w int, tail string) string {
+	return DefaultCondition.Truncate(s, w, tail)
+}
+
+// Wrap return string wrapped with w cells
+func Wrap(s string, w int) string {
+	return DefaultCondition.Wrap(s, w)
+}
+
+// FillLeft return string filled in left by spaces in w cells
+func FillLeft(s string, w int) string {
+	return DefaultCondition.FillLeft(s, w)
+}
+
+// FillRight return string filled in left by spaces in w cells
+func FillRight(s string, w int) string {
+	return DefaultCondition.FillRight(s, w)
+}
diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_js.go b/vendor/github.com/mattn/go-runewidth/runewidth_js.go
new file mode 100644
index 0000000000000000000000000000000000000000..0ce32c5e7b71b9cb5326226e8d6ecc85057f3ea9
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/runewidth_js.go
@@ -0,0 +1,8 @@
+// +build js
+
+package runewidth
+
+func IsEastAsian() bool {
+	// TODO: Implement this for the web. Detect east asian in a compatible way, and return true.
+	return false
+}
diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_posix.go b/vendor/github.com/mattn/go-runewidth/runewidth_posix.go
new file mode 100644
index 0000000000000000000000000000000000000000..c579e9a31443dafcb7f9e64e139afa962ca8e8a8
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/runewidth_posix.go
@@ -0,0 +1,77 @@
+// +build !windows,!js
+
+package runewidth
+
+import (
+	"os"
+	"regexp"
+	"strings"
+)
+
+var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`)
+
+var mblenTable = map[string]int{
+	"utf-8":   6,
+	"utf8":    6,
+	"jis":     8,
+	"eucjp":   3,
+	"euckr":   2,
+	"euccn":   2,
+	"sjis":    2,
+	"cp932":   2,
+	"cp51932": 2,
+	"cp936":   2,
+	"cp949":   2,
+	"cp950":   2,
+	"big5":    2,
+	"gbk":     2,
+	"gb2312":  2,
+}
+
+func isEastAsian(locale string) bool {
+	charset := strings.ToLower(locale)
+	r := reLoc.FindStringSubmatch(locale)
+	if len(r) == 2 {
+		charset = strings.ToLower(r[1])
+	}
+
+	if strings.HasSuffix(charset, "@cjk_narrow") {
+		return false
+	}
+
+	for pos, b := range []byte(charset) {
+		if b == '@' {
+			charset = charset[:pos]
+			break
+		}
+	}
+	max := 1
+	if m, ok := mblenTable[charset]; ok {
+		max = m
+	}
+	if max > 1 && (charset[0] != 'u' ||
+		strings.HasPrefix(locale, "ja") ||
+		strings.HasPrefix(locale, "ko") ||
+		strings.HasPrefix(locale, "zh")) {
+		return true
+	}
+	return false
+}
+
+// IsEastAsian return true if the current locale is CJK
+func IsEastAsian() bool {
+	locale := os.Getenv("LC_CTYPE")
+	if locale == "" {
+		locale = os.Getenv("LANG")
+	}
+
+	// ignore C locale
+	if locale == "POSIX" || locale == "C" {
+		return false
+	}
+	if len(locale) > 1 && locale[0] == 'C' && (locale[1] == '.' || locale[1] == '-') {
+		return false
+	}
+
+	return isEastAsian(locale)
+}
diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_windows.go b/vendor/github.com/mattn/go-runewidth/runewidth_windows.go
new file mode 100644
index 0000000000000000000000000000000000000000..0258876b99dd024c5986b2c3de23ff20a3ad8db3
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/runewidth_windows.go
@@ -0,0 +1,25 @@
+package runewidth
+
+import (
+	"syscall"
+)
+
+var (
+	kernel32               = syscall.NewLazyDLL("kernel32")
+	procGetConsoleOutputCP = kernel32.NewProc("GetConsoleOutputCP")
+)
+
+// IsEastAsian return true if the current locale is CJK
+func IsEastAsian() bool {
+	r1, _, _ := procGetConsoleOutputCP.Call()
+	if r1 == 0 {
+		return false
+	}
+
+	switch int(r1) {
+	case 932, 51932, 936, 949, 950:
+		return true
+	}
+
+	return false
+}
diff --git a/vendor/github.com/olekukonko/tablewriter/.travis.yml b/vendor/github.com/olekukonko/tablewriter/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f156b3b87510e4fb151e3d02078ac454f37e521e
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/.travis.yml
@@ -0,0 +1,12 @@
+language: go
+
+go:
+  - 1.1
+  - 1.2
+  - 1.3
+  - 1.4
+  - 1.5
+  - 1.6
+  - 1.7
+  - 1.8
+  - tip
diff --git a/vendor/github.com/olekukonko/tablewriter/LICENCE.md b/vendor/github.com/olekukonko/tablewriter/LICENCE.md
new file mode 100644
index 0000000000000000000000000000000000000000..1fd8484253fbe1967b1efb11b1a0c4d8e76d4648
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/LICENCE.md
@@ -0,0 +1,19 @@
+Copyright (C) 2014 by Oleku Konko
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/vendor/github.com/olekukonko/tablewriter/README.md b/vendor/github.com/olekukonko/tablewriter/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7c36481366775f3626ec4ed606c18c16deeb403
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/README.md
@@ -0,0 +1,277 @@
+ASCII Table Writer
+=========
+
+[![Build Status](https://travis-ci.org/olekukonko/tablewriter.png?branch=master)](https://travis-ci.org/olekukonko/tablewriter) 
+[![Total views](https://img.shields.io/sourcegraph/rrc/github.com/olekukonko/tablewriter.svg)](https://sourcegraph.com/github.com/olekukonko/tablewriter)
+[![Godoc](https://godoc.org/github.com/olekukonko/tablewriter?status.svg)](https://godoc.org/github.com/olekukonko/tablewriter)
+
+Generate ASCII table on the fly ...  Installation is simple as
+
+    go get  github.com/olekukonko/tablewriter
+
+
+#### Features
+- Automatic Padding
+- Support Multiple Lines
+- Supports Alignment
+- Support Custom Separators
+- Automatic Alignment of numbers & percentage
+- Write directly to http , file etc via `io.Writer`
+- Read directly from CSV file
+- Optional row line via `SetRowLine`
+- Normalise table header
+- Make CSV Headers optional
+- Enable or disable table border
+- Set custom footer support
+- Optional identical cells merging
+- Set custom caption
+- Optional reflowing of paragrpahs in multi-line cells.
+
+#### Example   1 - Basic
+```go
+data := [][]string{
+    []string{"A", "The Good", "500"},
+    []string{"B", "The Very very Bad Man", "288"},
+    []string{"C", "The Ugly", "120"},
+    []string{"D", "The Gopher", "800"},
+}
+
+table := tablewriter.NewWriter(os.Stdout)
+table.SetHeader([]string{"Name", "Sign", "Rating"})
+
+for _, v := range data {
+    table.Append(v)
+}
+table.Render() // Send output
+```
+
+##### Output  1
+```
++------+-----------------------+--------+
+| NAME |         SIGN          | RATING |
++------+-----------------------+--------+
+|  A   |       The Good        |    500 |
+|  B   | The Very very Bad Man |    288 |
+|  C   |       The Ugly        |    120 |
+|  D   |      The Gopher       |    800 |
++------+-----------------------+--------+
+```
+
+#### Example 2 - Without Border / Footer / Bulk Append
+```go
+data := [][]string{
+    []string{"1/1/2014", "Domain name", "2233", "$10.98"},
+    []string{"1/1/2014", "January Hosting", "2233", "$54.95"},
+    []string{"1/4/2014", "February Hosting", "2233", "$51.00"},
+    []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
+}
+
+table := tablewriter.NewWriter(os.Stdout)
+table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
+table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer
+table.SetBorder(false)                                // Set Border to false
+table.AppendBulk(data)                                // Add Bulk Data
+table.Render()
+```
+
+##### Output 2
+```
+
+    DATE   |       DESCRIPTION        |  CV2  | AMOUNT
++----------+--------------------------+-------+---------+
+  1/1/2014 | Domain name              |  2233 | $10.98
+  1/1/2014 | January Hosting          |  2233 | $54.95
+  1/4/2014 | February Hosting         |  2233 | $51.00
+  1/4/2014 | February Extra Bandwidth |  2233 | $30.00
++----------+--------------------------+-------+---------+
+                                        TOTAL | $146 93
+                                      +-------+---------+
+
+```
+
+
+#### Example 3 - CSV
+```go
+table, _ := tablewriter.NewCSV(os.Stdout, "test_info.csv", true)
+table.SetAlignment(tablewriter.ALIGN_LEFT)   // Set Alignment
+table.Render()
+```
+
+##### Output 3
+```
++----------+--------------+------+-----+---------+----------------+
+|  FIELD   |     TYPE     | NULL | KEY | DEFAULT |     EXTRA      |
++----------+--------------+------+-----+---------+----------------+
+| user_id  | smallint(5)  | NO   | PRI | NULL    | auto_increment |
+| username | varchar(10)  | NO   |     | NULL    |                |
+| password | varchar(100) | NO   |     | NULL    |                |
++----------+--------------+------+-----+---------+----------------+
+```
+
+#### Example 4  - Custom Separator
+```go
+table, _ := tablewriter.NewCSV(os.Stdout, "test.csv", true)
+table.SetRowLine(true)         // Enable row line
+
+// Change table lines
+table.SetCenterSeparator("*")
+table.SetColumnSeparator("‡")
+table.SetRowSeparator("-")
+
+table.SetAlignment(tablewriter.ALIGN_LEFT)
+table.Render()
+```
+
+##### Output 4
+```
+*------------*-----------*---------*
+╪ FIRST NAME ╪ LAST NAME ╪   SSN   ╪
+*------------*-----------*---------*
+╪ John       ╪ Barry     ╪ 123456  ╪
+*------------*-----------*---------*
+╪ Kathy      ╪ Smith     ╪ 687987  ╪
+*------------*-----------*---------*
+╪ Bob        ╪ McCornick ╪ 3979870 ╪
+*------------*-----------*---------*
+```
+
+#### Example 5 - Markdown Format
+```go
+data := [][]string{
+	[]string{"1/1/2014", "Domain name", "2233", "$10.98"},
+	[]string{"1/1/2014", "January Hosting", "2233", "$54.95"},
+	[]string{"1/4/2014", "February Hosting", "2233", "$51.00"},
+	[]string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
+}
+
+table := tablewriter.NewWriter(os.Stdout)
+table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
+table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
+table.SetCenterSeparator("|")
+table.AppendBulk(data) // Add Bulk Data
+table.Render()
+```
+
+##### Output 5
+```
+|   DATE   |       DESCRIPTION        | CV2  | AMOUNT |
+|----------|--------------------------|------|--------|
+| 1/1/2014 | Domain name              | 2233 | $10.98 |
+| 1/1/2014 | January Hosting          | 2233 | $54.95 |
+| 1/4/2014 | February Hosting         | 2233 | $51.00 |
+| 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 |
+```
+
+#### Example 6  - Identical cells merging
+```go
+data := [][]string{
+  []string{"1/1/2014", "Domain name", "1234", "$10.98"},
+  []string{"1/1/2014", "January Hosting", "2345", "$54.95"},
+  []string{"1/4/2014", "February Hosting", "3456", "$51.00"},
+  []string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"},
+}
+
+table := tablewriter.NewWriter(os.Stdout)
+table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
+table.SetFooter([]string{"", "", "Total", "$146.93"})
+table.SetAutoMergeCells(true)
+table.SetRowLine(true)
+table.AppendBulk(data)
+table.Render()
+```
+
+##### Output 6
+```
++----------+--------------------------+-------+---------+
+|   DATE   |       DESCRIPTION        |  CV2  | AMOUNT  |
++----------+--------------------------+-------+---------+
+| 1/1/2014 | Domain name              |  1234 | $10.98  |
++          +--------------------------+-------+---------+
+|          | January Hosting          |  2345 | $54.95  |
++----------+--------------------------+-------+---------+
+| 1/4/2014 | February Hosting         |  3456 | $51.00  |
++          +--------------------------+-------+---------+
+|          | February Extra Bandwidth |  4567 | $30.00  |
++----------+--------------------------+-------+---------+
+|                                       TOTAL | $146 93 |
++----------+--------------------------+-------+---------+
+```
+
+
+#### Table with color
+```go
+data := [][]string{
+	[]string{"1/1/2014", "Domain name", "2233", "$10.98"},
+	[]string{"1/1/2014", "January Hosting", "2233", "$54.95"},
+	[]string{"1/4/2014", "February Hosting", "2233", "$51.00"},
+	[]string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
+}
+
+table := tablewriter.NewWriter(os.Stdout)
+table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
+table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer
+table.SetBorder(false)                                // Set Border to false
+
+table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor},
+	tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor},
+	tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor},
+	tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor})
+
+table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
+	tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor},
+	tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
+	tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
+
+table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{},
+	tablewriter.Colors{tablewriter.Bold},
+	tablewriter.Colors{tablewriter.FgHiRedColor})
+
+table.AppendBulk(data)
+table.Render()
+```
+
+#### Table with color Output
+![Table with Color](https://cloud.githubusercontent.com/assets/6460392/21101956/bbc7b356-c0a1-11e6-9f36-dba694746efc.png)
+
+#### Example 6 - Set table caption
+```go
+data := [][]string{
+    []string{"A", "The Good", "500"},
+    []string{"B", "The Very very Bad Man", "288"},
+    []string{"C", "The Ugly", "120"},
+    []string{"D", "The Gopher", "800"},
+}
+
+table := tablewriter.NewWriter(os.Stdout)
+table.SetHeader([]string{"Name", "Sign", "Rating"})
+table.SetCaption(true, "Movie ratings.")
+
+for _, v := range data {
+    table.Append(v)
+}
+table.Render() // Send output
+```
+
+Note: Caption text will wrap with total width of rendered table.
+
+##### Output 6
+```
++------+-----------------------+--------+
+| NAME |         SIGN          | RATING |
++------+-----------------------+--------+
+|  A   |       The Good        |    500 |
+|  B   | The Very very Bad Man |    288 |
+|  C   |       The Ugly        |    120 |
+|  D   |      The Gopher       |    800 |
++------+-----------------------+--------+
+Movie ratings.
+```
+
+#### TODO
+- ~~Import Directly from CSV~~  - `done`
+- ~~Support for `SetFooter`~~  - `done`
+- ~~Support for `SetBorder`~~  - `done`
+- ~~Support table with uneven rows~~ - `done`
+- ~~Support custom alignment~~
+- General Improvement & Optimisation
+- `NewHTML` Parse table from HTML
diff --git a/vendor/github.com/olekukonko/tablewriter/csv.go b/vendor/github.com/olekukonko/tablewriter/csv.go
new file mode 100644
index 0000000000000000000000000000000000000000..98878303bc48dc8bb40a7131f00d467018112621
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/csv.go
@@ -0,0 +1,52 @@
+// Copyright 2014 Oleku Konko All rights reserved.
+// Use of this source code is governed by a MIT
+// license that can be found in the LICENSE file.
+
+// This module is a Table Writer  API for the Go Programming Language.
+// The protocols were written in pure Go and works on windows and unix systems
+
+package tablewriter
+
+import (
+	"encoding/csv"
+	"io"
+	"os"
+)
+
+// Start A new table by importing from a CSV file
+// Takes io.Writer and csv File name
+func NewCSV(writer io.Writer, fileName string, hasHeader bool) (*Table, error) {
+	file, err := os.Open(fileName)
+	if err != nil {
+		return &Table{}, err
+	}
+	defer file.Close()
+	csvReader := csv.NewReader(file)
+	t, err := NewCSVReader(writer, csvReader, hasHeader)
+	return t, err
+}
+
+//  Start a New Table Writer with csv.Reader
+// This enables customisation such as reader.Comma = ';'
+// See http://golang.org/src/pkg/encoding/csv/reader.go?s=3213:3671#L94
+func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool) (*Table, error) {
+	t := NewWriter(writer)
+	if hasHeader {
+		// Read the first row
+		headers, err := csvReader.Read()
+		if err != nil {
+			return &Table{}, err
+		}
+		t.SetHeader(headers)
+	}
+	for {
+		record, err := csvReader.Read()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			return &Table{}, err
+		}
+		t.Append(record)
+	}
+	return t, nil
+}
diff --git a/vendor/github.com/olekukonko/tablewriter/table.go b/vendor/github.com/olekukonko/tablewriter/table.go
new file mode 100644
index 0000000000000000000000000000000000000000..6bbef96a81a3c55daafbf2ccb4d84ca5a3d11d97
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/table.go
@@ -0,0 +1,839 @@
+// Copyright 2014 Oleku Konko All rights reserved.
+// Use of this source code is governed by a MIT
+// license that can be found in the LICENSE file.
+
+// This module is a Table Writer  API for the Go Programming Language.
+// The protocols were written in pure Go and works on windows and unix systems
+
+// Create & Generate text based table
+package tablewriter
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"regexp"
+	"strings"
+)
+
+const (
+	MAX_ROW_WIDTH = 30
+)
+
+const (
+	CENTER  = "+"
+	ROW     = "-"
+	COLUMN  = "|"
+	SPACE   = " "
+	NEWLINE = "\n"
+)
+
+const (
+	ALIGN_DEFAULT = iota
+	ALIGN_CENTER
+	ALIGN_RIGHT
+	ALIGN_LEFT
+)
+
+var (
+	decimal = regexp.MustCompile(`^-*\d*\.?\d*$`)
+	percent = regexp.MustCompile(`^-*\d*\.?\d*$%$`)
+)
+
+type Border struct {
+	Left   bool
+	Right  bool
+	Top    bool
+	Bottom bool
+}
+
+type Table struct {
+	out            io.Writer
+	rows           [][]string
+	lines          [][][]string
+	cs             map[int]int
+	rs             map[int]int
+	headers        [][]string
+	footers        [][]string
+	caption        bool
+	captionText    string
+	autoFmt        bool
+	autoWrap       bool
+	reflowText     bool
+	mW             int
+	pCenter        string
+	pRow           string
+	pColumn        string
+	tColumn        int
+	tRow           int
+	hAlign         int
+	fAlign         int
+	align          int
+	newLine        string
+	rowLine        bool
+	autoMergeCells bool
+	hdrLine        bool
+	borders        Border
+	colSize        int
+	headerParams   []string
+	columnsParams  []string
+	footerParams   []string
+	columnsAlign   []int
+}
+
+// Start New Table
+// Take io.Writer Directly
+func NewWriter(writer io.Writer) *Table {
+	t := &Table{
+		out:           writer,
+		rows:          [][]string{},
+		lines:         [][][]string{},
+		cs:            make(map[int]int),
+		rs:            make(map[int]int),
+		headers:       [][]string{},
+		footers:       [][]string{},
+		caption:       false,
+		captionText:   "Table caption.",
+		autoFmt:       true,
+		autoWrap:      true,
+		reflowText:    true,
+		mW:            MAX_ROW_WIDTH,
+		pCenter:       CENTER,
+		pRow:          ROW,
+		pColumn:       COLUMN,
+		tColumn:       -1,
+		tRow:          -1,
+		hAlign:        ALIGN_DEFAULT,
+		fAlign:        ALIGN_DEFAULT,
+		align:         ALIGN_DEFAULT,
+		newLine:       NEWLINE,
+		rowLine:       false,
+		hdrLine:       true,
+		borders:       Border{Left: true, Right: true, Bottom: true, Top: true},
+		colSize:       -1,
+		headerParams:  []string{},
+		columnsParams: []string{},
+		footerParams:  []string{},
+		columnsAlign:  []int{}}
+	return t
+}
+
+// Render table output
+func (t *Table) Render() {
+	if t.borders.Top {
+		t.printLine(true)
+	}
+	t.printHeading()
+	if t.autoMergeCells {
+		t.printRowsMergeCells()
+	} else {
+		t.printRows()
+	}
+	if !t.rowLine && t.borders.Bottom {
+		t.printLine(true)
+	}
+	t.printFooter()
+
+	if t.caption {
+		t.printCaption()
+	}
+}
+
+const (
+	headerRowIdx = -1
+	footerRowIdx = -2
+)
+
+// Set table header
+func (t *Table) SetHeader(keys []string) {
+	t.colSize = len(keys)
+	for i, v := range keys {
+		lines := t.parseDimension(v, i, headerRowIdx)
+		t.headers = append(t.headers, lines)
+	}
+}
+
+// Set table Footer
+func (t *Table) SetFooter(keys []string) {
+	//t.colSize = len(keys)
+	for i, v := range keys {
+		lines := t.parseDimension(v, i, footerRowIdx)
+		t.footers = append(t.footers, lines)
+	}
+}
+
+// Set table Caption
+func (t *Table) SetCaption(caption bool, captionText ...string) {
+	t.caption = caption
+	if len(captionText) == 1 {
+		t.captionText = captionText[0]
+	}
+}
+
+// Turn header autoformatting on/off. Default is on (true).
+func (t *Table) SetAutoFormatHeaders(auto bool) {
+	t.autoFmt = auto
+}
+
+// Turn automatic multiline text adjustment on/off. Default is on (true).
+func (t *Table) SetAutoWrapText(auto bool) {
+	t.autoWrap = auto
+}
+
+// Turn automatic reflowing of multiline text when rewrapping. Default is on (true).
+func (t *Table) SetReflowDuringAutoWrap(auto bool) {
+	t.reflowText = auto
+}
+
+// Set the Default column width
+func (t *Table) SetColWidth(width int) {
+	t.mW = width
+}
+
+// Set the minimal width for a column
+func (t *Table) SetColMinWidth(column int, width int) {
+	t.cs[column] = width
+}
+
+// Set the Column Separator
+func (t *Table) SetColumnSeparator(sep string) {
+	t.pColumn = sep
+}
+
+// Set the Row Separator
+func (t *Table) SetRowSeparator(sep string) {
+	t.pRow = sep
+}
+
+// Set the center Separator
+func (t *Table) SetCenterSeparator(sep string) {
+	t.pCenter = sep
+}
+
+// Set Header Alignment
+func (t *Table) SetHeaderAlignment(hAlign int) {
+	t.hAlign = hAlign
+}
+
+// Set Footer Alignment
+func (t *Table) SetFooterAlignment(fAlign int) {
+	t.fAlign = fAlign
+}
+
+// Set Table Alignment
+func (t *Table) SetAlignment(align int) {
+	t.align = align
+}
+
+func (t *Table) SetColumnAlignment(keys []int) {
+	for _, v := range keys {
+		switch v {
+		case ALIGN_CENTER:
+			break
+		case ALIGN_LEFT:
+			break
+		case ALIGN_RIGHT:
+			break
+		default:
+			v = ALIGN_DEFAULT
+		}
+		t.columnsAlign = append(t.columnsAlign, v)
+	}
+}
+
+// Set New Line
+func (t *Table) SetNewLine(nl string) {
+	t.newLine = nl
+}
+
+// Set Header Line
+// This would enable / disable a line after the header
+func (t *Table) SetHeaderLine(line bool) {
+	t.hdrLine = line
+}
+
+// Set Row Line
+// This would enable / disable a line on each row of the table
+func (t *Table) SetRowLine(line bool) {
+	t.rowLine = line
+}
+
+// Set Auto Merge Cells
+// This would enable / disable the merge of cells with identical values
+func (t *Table) SetAutoMergeCells(auto bool) {
+	t.autoMergeCells = auto
+}
+
+// Set Table Border
+// This would enable / disable line around the table
+func (t *Table) SetBorder(border bool) {
+	t.SetBorders(Border{border, border, border, border})
+}
+
+func (t *Table) SetBorders(border Border) {
+	t.borders = border
+}
+
+// Append row to table
+func (t *Table) Append(row []string) {
+	rowSize := len(t.headers)
+	if rowSize > t.colSize {
+		t.colSize = rowSize
+	}
+
+	n := len(t.lines)
+	line := [][]string{}
+	for i, v := range row {
+
+		// Detect string  width
+		// Detect String height
+		// Break strings into words
+		out := t.parseDimension(v, i, n)
+
+		// Append broken words
+		line = append(line, out)
+	}
+	t.lines = append(t.lines, line)
+}
+
+// Allow Support for Bulk Append
+// Eliminates repeated for loops
+func (t *Table) AppendBulk(rows [][]string) {
+	for _, row := range rows {
+		t.Append(row)
+	}
+}
+
+// NumLines to get the number of lines
+func (t *Table) NumLines() int {
+	return len(t.lines)
+}
+
+// Clear rows
+func (t *Table) ClearRows() {
+	t.lines = [][][]string{}
+}
+
+// Clear footer
+func (t *Table) ClearFooter() {
+	t.footers = [][]string{}
+}
+
+// Print line based on row width
+func (t *Table) printLine(nl bool) {
+	fmt.Fprint(t.out, t.pCenter)
+	for i := 0; i < len(t.cs); i++ {
+		v := t.cs[i]
+		fmt.Fprintf(t.out, "%s%s%s%s",
+			t.pRow,
+			strings.Repeat(string(t.pRow), v),
+			t.pRow,
+			t.pCenter)
+	}
+	if nl {
+		fmt.Fprint(t.out, t.newLine)
+	}
+}
+
+// Print line based on row width with our without cell separator
+func (t *Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) {
+	fmt.Fprint(t.out, t.pCenter)
+	for i := 0; i < len(t.cs); i++ {
+		v := t.cs[i]
+		if i > len(displayCellSeparator) || displayCellSeparator[i] {
+			// Display the cell separator
+			fmt.Fprintf(t.out, "%s%s%s%s",
+				t.pRow,
+				strings.Repeat(string(t.pRow), v),
+				t.pRow,
+				t.pCenter)
+		} else {
+			// Don't display the cell separator for this cell
+			fmt.Fprintf(t.out, "%s%s",
+				strings.Repeat(" ", v+2),
+				t.pCenter)
+		}
+	}
+	if nl {
+		fmt.Fprint(t.out, t.newLine)
+	}
+}
+
+// Return the PadRight function if align is left, PadLeft if align is right,
+// and Pad by default
+func pad(align int) func(string, string, int) string {
+	padFunc := Pad
+	switch align {
+	case ALIGN_LEFT:
+		padFunc = PadRight
+	case ALIGN_RIGHT:
+		padFunc = PadLeft
+	}
+	return padFunc
+}
+
+// Print heading information
+func (t *Table) printHeading() {
+	// Check if headers is available
+	if len(t.headers) < 1 {
+		return
+	}
+
+	// Identify last column
+	end := len(t.cs) - 1
+
+	// Get pad function
+	padFunc := pad(t.hAlign)
+
+	// Checking for ANSI escape sequences for header
+	is_esc_seq := false
+	if len(t.headerParams) > 0 {
+		is_esc_seq = true
+	}
+
+	// Maximum height.
+	max := t.rs[headerRowIdx]
+
+	// Print Heading
+	for x := 0; x < max; x++ {
+		// Check if border is set
+		// Replace with space if not set
+		fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))
+
+		for y := 0; y <= end; y++ {
+			v := t.cs[y]
+			h := ""
+			if y < len(t.headers) && x < len(t.headers[y]) {
+				h = t.headers[y][x]
+			}
+			if t.autoFmt {
+				h = Title(h)
+			}
+			pad := ConditionString((y == end && !t.borders.Left), SPACE, t.pColumn)
+
+			if is_esc_seq {
+				fmt.Fprintf(t.out, " %s %s",
+					format(padFunc(h, SPACE, v),
+						t.headerParams[y]), pad)
+			} else {
+				fmt.Fprintf(t.out, " %s %s",
+					padFunc(h, SPACE, v),
+					pad)
+			}
+		}
+		// Next line
+		fmt.Fprint(t.out, t.newLine)
+	}
+	if t.hdrLine {
+		t.printLine(true)
+	}
+}
+
+// Print heading information
+func (t *Table) printFooter() {
+	// Check if headers is available
+	if len(t.footers) < 1 {
+		return
+	}
+
+	// Only print line if border is not set
+	if !t.borders.Bottom {
+		t.printLine(true)
+	}
+
+	// Identify last column
+	end := len(t.cs) - 1
+
+	// Get pad function
+	padFunc := pad(t.fAlign)
+
+	// Checking for ANSI escape sequences for header
+	is_esc_seq := false
+	if len(t.footerParams) > 0 {
+		is_esc_seq = true
+	}
+
+	// Maximum height.
+	max := t.rs[footerRowIdx]
+
+	// Print Footer
+	erasePad := make([]bool, len(t.footers))
+	for x := 0; x < max; x++ {
+		// Check if border is set
+		// Replace with space if not set
+		fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE))
+
+		for y := 0; y <= end; y++ {
+			v := t.cs[y]
+			f := ""
+			if y < len(t.footers) && x < len(t.footers[y]) {
+				f = t.footers[y][x]
+			}
+			if t.autoFmt {
+				f = Title(f)
+			}
+			pad := ConditionString((y == end && !t.borders.Top), SPACE, t.pColumn)
+
+			if erasePad[y] || (x == 0 && len(f) == 0) {
+				pad = SPACE
+				erasePad[y] = true
+			}
+
+			if is_esc_seq {
+				fmt.Fprintf(t.out, " %s %s",
+					format(padFunc(f, SPACE, v),
+						t.footerParams[y]), pad)
+			} else {
+				fmt.Fprintf(t.out, " %s %s",
+					padFunc(f, SPACE, v),
+					pad)
+			}
+
+			//fmt.Fprintf(t.out, " %s %s",
+			//	padFunc(f, SPACE, v),
+			//	pad)
+		}
+		// Next line
+		fmt.Fprint(t.out, t.newLine)
+		//t.printLine(true)
+	}
+
+	hasPrinted := false
+
+	for i := 0; i <= end; i++ {
+		v := t.cs[i]
+		pad := t.pRow
+		center := t.pCenter
+		length := len(t.footers[i][0])
+
+		if length > 0 {
+			hasPrinted = true
+		}
+
+		// Set center to be space if length is 0
+		if length == 0 && !t.borders.Right {
+			center = SPACE
+		}
+
+		// Print first junction
+		if i == 0 {
+			fmt.Fprint(t.out, center)
+		}
+
+		// Pad With space of length is 0
+		if length == 0 {
+			pad = SPACE
+		}
+		// Ignore left space of it has printed before
+		if hasPrinted || t.borders.Left {
+			pad = t.pRow
+			center = t.pCenter
+		}
+
+		// Change Center start position
+		if center == SPACE {
+			if i < end && len(t.footers[i+1][0]) != 0 {
+				center = t.pCenter
+			}
+		}
+
+		// Print the footer
+		fmt.Fprintf(t.out, "%s%s%s%s",
+			pad,
+			strings.Repeat(string(pad), v),
+			pad,
+			center)
+
+	}
+
+	fmt.Fprint(t.out, t.newLine)
+}
+
+// Print caption text
+func (t Table) printCaption() {
+	width := t.getTableWidth()
+	paragraph, _ := WrapString(t.captionText, width)
+	for linecount := 0; linecount < len(paragraph); linecount++ {
+		fmt.Fprintln(t.out, paragraph[linecount])
+	}
+}
+
+// Calculate the total number of characters in a row
+func (t Table) getTableWidth() int {
+	var chars int
+	for _, v := range t.cs {
+		chars += v
+	}
+
+	// Add chars, spaces, seperators to calculate the total width of the table.
+	// ncols := t.colSize
+	// spaces := ncols * 2
+	// seps := ncols + 1
+
+	return (chars + (3 * t.colSize) + 2)
+}
+
+func (t Table) printRows() {
+	for i, lines := range t.lines {
+		t.printRow(lines, i)
+	}
+}
+
+func (t *Table) fillAlignment(num int) {
+	if len(t.columnsAlign) < num {
+		t.columnsAlign = make([]int, num)
+		for i := range t.columnsAlign {
+			t.columnsAlign[i] = t.align
+		}
+	}
+}
+
+// Print Row Information
+// Adjust column alignment based on type
+
+func (t *Table) printRow(columns [][]string, rowIdx int) {
+	// Get Maximum Height
+	max := t.rs[rowIdx]
+	total := len(columns)
+
+	// TODO Fix uneven col size
+	// if total < t.colSize {
+	//	for n := t.colSize - total; n < t.colSize ; n++ {
+	//		columns = append(columns, []string{SPACE})
+	//		t.cs[n] = t.mW
+	//	}
+	//}
+
+	// Pad Each Height
+	pads := []int{}
+
+	// Checking for ANSI escape sequences for columns
+	is_esc_seq := false
+	if len(t.columnsParams) > 0 {
+		is_esc_seq = true
+	}
+	t.fillAlignment(total)
+
+	for i, line := range columns {
+		length := len(line)
+		pad := max - length
+		pads = append(pads, pad)
+		for n := 0; n < pad; n++ {
+			columns[i] = append(columns[i], "  ")
+		}
+	}
+	//fmt.Println(max, "\n")
+	for x := 0; x < max; x++ {
+		for y := 0; y < total; y++ {
+
+			// Check if border is set
+			fmt.Fprint(t.out, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn))
+
+			fmt.Fprintf(t.out, SPACE)
+			str := columns[y][x]
+
+			// Embedding escape sequence with column value
+			if is_esc_seq {
+				str = format(str, t.columnsParams[y])
+			}
+
+			// This would print alignment
+			// Default alignment  would use multiple configuration
+			switch t.columnsAlign[y] {
+			case ALIGN_CENTER: //
+				fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y]))
+			case ALIGN_RIGHT:
+				fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y]))
+			case ALIGN_LEFT:
+				fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y]))
+			default:
+				if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) {
+					fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y]))
+				} else {
+					fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y]))
+
+					// TODO Custom alignment per column
+					//if max == 1 || pads[y] > 0 {
+					//	fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y]))
+					//} else {
+					//	fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y]))
+					//}
+
+				}
+			}
+			fmt.Fprintf(t.out, SPACE)
+		}
+		// Check if border is set
+		// Replace with space if not set
+		fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))
+		fmt.Fprint(t.out, t.newLine)
+	}
+
+	if t.rowLine {
+		t.printLine(true)
+	}
+}
+
+// Print the rows of the table and merge the cells that are identical
+func (t *Table) printRowsMergeCells() {
+	var previousLine []string
+	var displayCellBorder []bool
+	var tmpWriter bytes.Buffer
+	for i, lines := range t.lines {
+		// We store the display of the current line in a tmp writer, as we need to know which border needs to be print above
+		previousLine, displayCellBorder = t.printRowMergeCells(&tmpWriter, lines, i, previousLine)
+		if i > 0 { //We don't need to print borders above first line
+			if t.rowLine {
+				t.printLineOptionalCellSeparators(true, displayCellBorder)
+			}
+		}
+		tmpWriter.WriteTo(t.out)
+	}
+	//Print the end of the table
+	if t.rowLine {
+		t.printLine(true)
+	}
+}
+
+// Print Row Information to a writer and merge identical cells.
+// Adjust column alignment based on type
+
+func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, rowIdx int, previousLine []string) ([]string, []bool) {
+	// Get Maximum Height
+	max := t.rs[rowIdx]
+	total := len(columns)
+
+	// Pad Each Height
+	pads := []int{}
+
+	for i, line := range columns {
+		length := len(line)
+		pad := max - length
+		pads = append(pads, pad)
+		for n := 0; n < pad; n++ {
+			columns[i] = append(columns[i], "  ")
+		}
+	}
+
+	var displayCellBorder []bool
+	t.fillAlignment(total)
+	for x := 0; x < max; x++ {
+		for y := 0; y < total; y++ {
+
+			// Check if border is set
+			fmt.Fprint(writer, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn))
+
+			fmt.Fprintf(writer, SPACE)
+
+			str := columns[y][x]
+
+			if t.autoMergeCells {
+				//Store the full line to merge mutli-lines cells
+				fullLine := strings.Join(columns[y], " ")
+				if len(previousLine) > y && fullLine == previousLine[y] && fullLine != "" {
+					// If this cell is identical to the one above but not empty, we don't display the border and keep the cell empty.
+					displayCellBorder = append(displayCellBorder, false)
+					str = ""
+				} else {
+					// First line or different content, keep the content and print the cell border
+					displayCellBorder = append(displayCellBorder, true)
+				}
+			}
+
+			// This would print alignment
+			// Default alignment  would use multiple configuration
+			switch t.columnsAlign[y] {
+			case ALIGN_CENTER: //
+				fmt.Fprintf(writer, "%s", Pad(str, SPACE, t.cs[y]))
+			case ALIGN_RIGHT:
+				fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y]))
+			case ALIGN_LEFT:
+				fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y]))
+			default:
+				if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) {
+					fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y]))
+				} else {
+					fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y]))
+				}
+			}
+			fmt.Fprintf(writer, SPACE)
+		}
+		// Check if border is set
+		// Replace with space if not set
+		fmt.Fprint(writer, ConditionString(t.borders.Left, t.pColumn, SPACE))
+		fmt.Fprint(writer, t.newLine)
+	}
+
+	//The new previous line is the current one
+	previousLine = make([]string, total)
+	for y := 0; y < total; y++ {
+		previousLine[y] = strings.Join(columns[y], " ") //Store the full line for multi-lines cells
+	}
+	//Returns the newly added line and wether or not a border should be displayed above.
+	return previousLine, displayCellBorder
+}
+
+func (t *Table) parseDimension(str string, colKey, rowKey int) []string {
+	var (
+		raw      []string
+		maxWidth int
+	)
+
+	raw = getLines(str)
+	maxWidth = 0
+	for _, line := range raw {
+		if w := DisplayWidth(line); w > maxWidth {
+			maxWidth = w
+		}
+	}
+
+	// If wrapping, ensure that all paragraphs in the cell fit in the
+	// specified width.
+	if t.autoWrap {
+		// If there's a maximum allowed width for wrapping, use that.
+		if maxWidth > t.mW {
+			maxWidth = t.mW
+		}
+
+		// In the process of doing so, we need to recompute maxWidth. This
+		// is because perhaps a word in the cell is longer than the
+		// allowed maximum width in t.mW.
+		newMaxWidth := maxWidth
+		newRaw := make([]string, 0, len(raw))
+
+		if t.reflowText {
+			// Make a single paragraph of everything.
+			raw = []string{strings.Join(raw, " ")}
+		}
+		for i, para := range raw {
+			paraLines, _ := WrapString(para, maxWidth)
+			for _, line := range paraLines {
+				if w := DisplayWidth(line); w > newMaxWidth {
+					newMaxWidth = w
+				}
+			}
+			if i > 0 {
+				newRaw = append(newRaw, " ")
+			}
+			newRaw = append(newRaw, paraLines...)
+		}
+		raw = newRaw
+		maxWidth = newMaxWidth
+	}
+
+	// Store the new known maximum width.
+	v, ok := t.cs[colKey]
+	if !ok || v < maxWidth || v == 0 {
+		t.cs[colKey] = maxWidth
+	}
+
+	// Remember the number of lines for the row printer.
+	h := len(raw)
+	v, ok = t.rs[rowKey]
+
+	if !ok || v < h || v == 0 {
+		t.rs[rowKey] = h
+	}
+	//fmt.Printf("Raw %+v %d\n", raw, len(raw))
+	return raw
+}
diff --git a/vendor/github.com/olekukonko/tablewriter/table_with_color.go b/vendor/github.com/olekukonko/tablewriter/table_with_color.go
new file mode 100644
index 0000000000000000000000000000000000000000..5a4a53ec2a389a50a43030f653c3398247f5e7cb
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/table_with_color.go
@@ -0,0 +1,134 @@
+package tablewriter
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+const ESC = "\033"
+const SEP = ";"
+
+const (
+	BgBlackColor int = iota + 40
+	BgRedColor
+	BgGreenColor
+	BgYellowColor
+	BgBlueColor
+	BgMagentaColor
+	BgCyanColor
+	BgWhiteColor
+)
+
+const (
+	FgBlackColor int = iota + 30
+	FgRedColor
+	FgGreenColor
+	FgYellowColor
+	FgBlueColor
+	FgMagentaColor
+	FgCyanColor
+	FgWhiteColor
+)
+
+const (
+	BgHiBlackColor int = iota + 100
+	BgHiRedColor
+	BgHiGreenColor
+	BgHiYellowColor
+	BgHiBlueColor
+	BgHiMagentaColor
+	BgHiCyanColor
+	BgHiWhiteColor
+)
+
+const (
+	FgHiBlackColor int = iota + 90
+	FgHiRedColor
+	FgHiGreenColor
+	FgHiYellowColor
+	FgHiBlueColor
+	FgHiMagentaColor
+	FgHiCyanColor
+	FgHiWhiteColor
+)
+
+const (
+	Normal          = 0
+	Bold            = 1
+	UnderlineSingle = 4
+	Italic
+)
+
+type Colors []int
+
+func startFormat(seq string) string {
+	return fmt.Sprintf("%s[%sm", ESC, seq)
+}
+
+func stopFormat() string {
+	return fmt.Sprintf("%s[%dm", ESC, Normal)
+}
+
+// Making the SGR (Select Graphic Rendition) sequence.
+func makeSequence(codes []int) string {
+	codesInString := []string{}
+	for _, code := range codes {
+		codesInString = append(codesInString, strconv.Itoa(code))
+	}
+	return strings.Join(codesInString, SEP)
+}
+
+// Adding ANSI escape  sequences before and after string
+func format(s string, codes interface{}) string {
+	var seq string
+
+	switch v := codes.(type) {
+
+	case string:
+		seq = v
+	case []int:
+		seq = makeSequence(v)
+	default:
+		return s
+	}
+
+	if len(seq) == 0 {
+		return s
+	}
+	return startFormat(seq) + s + stopFormat()
+}
+
+// Adding header colors (ANSI codes)
+func (t *Table) SetHeaderColor(colors ...Colors) {
+	if t.colSize != len(colors) {
+		panic("Number of header colors must be equal to number of headers.")
+	}
+	for i := 0; i < len(colors); i++ {
+		t.headerParams = append(t.headerParams, makeSequence(colors[i]))
+	}
+}
+
+// Adding column colors (ANSI codes)
+func (t *Table) SetColumnColor(colors ...Colors) {
+	if t.colSize != len(colors) {
+		panic("Number of column colors must be equal to number of headers.")
+	}
+	for i := 0; i < len(colors); i++ {
+		t.columnsParams = append(t.columnsParams, makeSequence(colors[i]))
+	}
+}
+
+// Adding column colors (ANSI codes)
+func (t *Table) SetFooterColor(colors ...Colors) {
+	if len(t.footers) != len(colors) {
+		panic("Number of footer colors must be equal to number of footer.")
+	}
+	for i := 0; i < len(colors); i++ {
+		t.footerParams = append(t.footerParams, makeSequence(colors[i]))
+	}
+}
+
+func Color(colors ...int) []int {
+	return colors
+}
diff --git a/vendor/github.com/olekukonko/tablewriter/test.csv b/vendor/github.com/olekukonko/tablewriter/test.csv
new file mode 100644
index 0000000000000000000000000000000000000000..1609327e930f415b650e44bbb3e7fa61533b98ae
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/test.csv
@@ -0,0 +1,4 @@
+first_name,last_name,ssn
+John,Barry,123456
+Kathy,Smith,687987
+Bob,McCornick,3979870
\ No newline at end of file
diff --git a/vendor/github.com/olekukonko/tablewriter/test_info.csv b/vendor/github.com/olekukonko/tablewriter/test_info.csv
new file mode 100644
index 0000000000000000000000000000000000000000..e4c40e983aca8486e3d2b714db4abdd7f9497a77
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/test_info.csv
@@ -0,0 +1,4 @@
+Field,Type,Null,Key,Default,Extra
+user_id,smallint(5),NO,PRI,NULL,auto_increment
+username,varchar(10),NO,,NULL, 
+password,varchar(100),NO,,NULL, 
\ No newline at end of file
diff --git a/vendor/github.com/olekukonko/tablewriter/util.go b/vendor/github.com/olekukonko/tablewriter/util.go
new file mode 100644
index 0000000000000000000000000000000000000000..dea3c7aaf46b6da2ead12b4f90e9fc94388cd5e3
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/util.go
@@ -0,0 +1,78 @@
+// Copyright 2014 Oleku Konko All rights reserved.
+// Use of this source code is governed by a MIT
+// license that can be found in the LICENSE file.
+
+// This module is a Table Writer  API for the Go Programming Language.
+// The protocols were written in pure Go and works on windows and unix systems
+
+package tablewriter
+
+import (
+	"math"
+	"regexp"
+	"strings"
+
+	"github.com/mattn/go-runewidth"
+)
+
+var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
+
+func DisplayWidth(str string) int {
+	return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
+}
+
+// Simple Condition for string
+// Returns value based on condition
+func ConditionString(cond bool, valid, inValid string) string {
+	if cond {
+		return valid
+	}
+	return inValid
+}
+
+// Format Table Header
+// Replace _ , . and spaces
+func Title(name string) string {
+	origLen := len(name)
+	name = strings.Replace(name, "_", " ", -1)
+	name = strings.Replace(name, ".", " ", -1)
+	name = strings.TrimSpace(name)
+	if len(name) == 0 && origLen > 0 {
+		// Keep at least one character. This is important to preserve
+		// empty lines in multi-line headers/footers.
+		name = " "
+	}
+	return strings.ToUpper(name)
+}
+
+// Pad String
+// Attempts to play string in the center
+func Pad(s, pad string, width int) string {
+	gap := width - DisplayWidth(s)
+	if gap > 0 {
+		gapLeft := int(math.Ceil(float64(gap / 2)))
+		gapRight := gap - gapLeft
+		return strings.Repeat(string(pad), gapLeft) + s + strings.Repeat(string(pad), gapRight)
+	}
+	return s
+}
+
+// Pad String Right position
+// This would pace string at the left side fo the screen
+func PadRight(s, pad string, width int) string {
+	gap := width - DisplayWidth(s)
+	if gap > 0 {
+		return s + strings.Repeat(string(pad), gap)
+	}
+	return s
+}
+
+// Pad String Left position
+// This would pace string at the right side fo the screen
+func PadLeft(s, pad string, width int) string {
+	gap := width - DisplayWidth(s)
+	if gap > 0 {
+		return strings.Repeat(string(pad), gap) + s
+	}
+	return s
+}
diff --git a/vendor/github.com/olekukonko/tablewriter/wrap.go b/vendor/github.com/olekukonko/tablewriter/wrap.go
new file mode 100644
index 0000000000000000000000000000000000000000..a092ee1f75d3e14096a1e2f2cce183de6e3cece6
--- /dev/null
+++ b/vendor/github.com/olekukonko/tablewriter/wrap.go
@@ -0,0 +1,99 @@
+// Copyright 2014 Oleku Konko All rights reserved.
+// Use of this source code is governed by a MIT
+// license that can be found in the LICENSE file.
+
+// This module is a Table Writer  API for the Go Programming Language.
+// The protocols were written in pure Go and works on windows and unix systems
+
+package tablewriter
+
+import (
+	"math"
+	"strings"
+
+	"github.com/mattn/go-runewidth"
+)
+
+var (
+	nl = "\n"
+	sp = " "
+)
+
+const defaultPenalty = 1e5
+
+// Wrap wraps s into a paragraph of lines of length lim, with minimal
+// raggedness.
+func WrapString(s string, lim int) ([]string, int) {
+	words := strings.Split(strings.Replace(s, nl, sp, -1), sp)
+	var lines []string
+	max := 0
+	for _, v := range words {
+		max = runewidth.StringWidth(v)
+		if max > lim {
+			lim = max
+		}
+	}
+	for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
+		lines = append(lines, strings.Join(line, sp))
+	}
+	return lines, lim
+}
+
+// WrapWords is the low-level line-breaking algorithm, useful if you need more
+// control over the details of the text wrapping process. For most uses,
+// WrapString will be sufficient and more convenient.
+//
+// WrapWords splits a list of words into lines with minimal "raggedness",
+// treating each rune as one unit, accounting for spc units between adjacent
+// words on each line, and attempting to limit lines to lim units. Raggedness
+// is the total error over all lines, where error is the square of the
+// difference of the length of the line and lim. Too-long lines (which only
+// happen when a single word is longer than lim units) have pen penalty units
+// added to the error.
+func WrapWords(words []string, spc, lim, pen int) [][]string {
+	n := len(words)
+
+	length := make([][]int, n)
+	for i := 0; i < n; i++ {
+		length[i] = make([]int, n)
+		length[i][i] = runewidth.StringWidth(words[i])
+		for j := i + 1; j < n; j++ {
+			length[i][j] = length[i][j-1] + spc + runewidth.StringWidth(words[j])
+		}
+	}
+	nbrk := make([]int, n)
+	cost := make([]int, n)
+	for i := range cost {
+		cost[i] = math.MaxInt32
+	}
+	for i := n - 1; i >= 0; i-- {
+		if length[i][n-1] <= lim {
+			cost[i] = 0
+			nbrk[i] = n
+		} else {
+			for j := i + 1; j < n; j++ {
+				d := lim - length[i][j-1]
+				c := d*d + cost[j]
+				if length[i][j-1] > lim {
+					c += pen // too-long lines get a worse penalty
+				}
+				if c < cost[i] {
+					cost[i] = c
+					nbrk[i] = j
+				}
+			}
+		}
+	}
+	var lines [][]string
+	i := 0
+	for i < n {
+		lines = append(lines, words[i:nbrk[i]])
+		i = nbrk[i]
+	}
+	return lines
+}
+
+// getLines decomposes a multiline string into a slice of strings.
+func getLines(s string) []string {
+	return strings.Split(s, nl)
+}
diff --git a/vendor/github.com/stretchr/objx/.gitignore b/vendor/github.com/stretchr/objx/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e0170a5f9f0450633f3b5543ca9ad1baefa4a916
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/.gitignore
@@ -0,0 +1,4 @@
+/dep
+/testdep
+/profile.out
+/coverage.txt
diff --git a/vendor/github.com/stretchr/objx/.travis.yml b/vendor/github.com/stretchr/objx/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1456363eadf20ddbf90e589b6ac3792dc6d889f1
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/.travis.yml
@@ -0,0 +1,13 @@
+language: go
+go:
+  - 1.8
+  - 1.9
+  - tip
+
+install:
+- go get github.com/go-task/task/cmd/task
+
+script:
+- task dl-deps
+- task lint
+- task test
diff --git a/vendor/github.com/stretchr/objx/Gopkg.lock b/vendor/github.com/stretchr/objx/Gopkg.lock
new file mode 100644
index 0000000000000000000000000000000000000000..1f5739c91df8f8dfdc9360053dde322d58634f32
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/Gopkg.lock
@@ -0,0 +1,27 @@
+# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
+
+
+[[projects]]
+  name = "github.com/davecgh/go-spew"
+  packages = ["spew"]
+  revision = "346938d642f2ec3594ed81d874461961cd0faa76"
+  version = "v1.1.0"
+
+[[projects]]
+  name = "github.com/pmezard/go-difflib"
+  packages = ["difflib"]
+  revision = "792786c7400a136282c1664665ae0a8db921c6c2"
+  version = "v1.0.0"
+
+[[projects]]
+  name = "github.com/stretchr/testify"
+  packages = ["assert"]
+  revision = "b91bfb9ebec76498946beb6af7c0230c7cc7ba6c"
+  version = "v1.2.0"
+
+[solve-meta]
+  analyzer-name = "dep"
+  analyzer-version = 1
+  inputs-digest = "50e2495ec1af6e2f7ffb2f3551e4300d30357d7c7fe38ff6056469fa9cfb3673"
+  solver-name = "gps-cdcl"
+  solver-version = 1
diff --git a/vendor/github.com/stretchr/objx/Gopkg.toml b/vendor/github.com/stretchr/objx/Gopkg.toml
new file mode 100644
index 0000000000000000000000000000000000000000..f87e18eb5499576af198e3668f7efe12a450b209
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/Gopkg.toml
@@ -0,0 +1,3 @@
+[[constraint]]
+  name = "github.com/stretchr/testify"
+  version = "~1.2.0"
diff --git a/vendor/github.com/stretchr/objx/LICENSE b/vendor/github.com/stretchr/objx/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..44d4d9d5a7c38acb84d299e75e108e197b3040df
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/LICENSE
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2014 Stretchr, Inc.
+Copyright (c) 2017-2018 objx contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/stretchr/objx/README.md b/vendor/github.com/stretchr/objx/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4e2400eb1f9d7ec32b131e6af7e6557e16f6747c
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/README.md
@@ -0,0 +1,78 @@
+# Objx
+[![Build Status](https://travis-ci.org/stretchr/objx.svg?branch=master)](https://travis-ci.org/stretchr/objx)
+[![Go Report Card](https://goreportcard.com/badge/github.com/stretchr/objx)](https://goreportcard.com/report/github.com/stretchr/objx)
+[![Sourcegraph](https://sourcegraph.com/github.com/stretchr/objx/-/badge.svg)](https://sourcegraph.com/github.com/stretchr/objx)
+[![GoDoc](https://godoc.org/github.com/stretchr/objx?status.svg)](https://godoc.org/github.com/stretchr/objx)
+
+Objx - Go package for dealing with maps, slices, JSON and other data.
+
+Get started:
+
+- Install Objx with [one line of code](#installation), or [update it with another](#staying-up-to-date)
+- Check out the API Documentation http://godoc.org/github.com/stretchr/objx
+
+## Overview
+Objx provides the `objx.Map` type, which is a `map[string]interface{}` that exposes a powerful `Get` method (among others) that allows you to easily and quickly get access to data within the map, without having to worry too much about type assertions, missing data, default values etc.
+
+### Pattern
+Objx uses a preditable pattern to make access data from within `map[string]interface{}` easy. Call one of the `objx.` functions to create your `objx.Map` to get going:
+
+    m, err := objx.FromJSON(json)
+
+NOTE: Any methods or functions with the `Must` prefix will panic if something goes wrong, the rest will be optimistic and try to figure things out without panicking.
+
+Use `Get` to access the value you're interested in.  You can use dot and array
+notation too:
+
+     m.Get("places[0].latlng")
+
+Once you have sought the `Value` you're interested in, you can use the `Is*` methods to determine its type.
+
+     if m.Get("code").IsStr() { // Your code... }
+
+Or you can just assume the type, and use one of the strong type methods to extract the real value:
+
+    m.Get("code").Int()
+
+If there's no value there (or if it's the wrong type) then a default value will be returned, or you can be explicit about the default value.
+
+     Get("code").Int(-1)
+
+If you're dealing with a slice of data as a value, Objx provides many useful methods for iterating, manipulating and selecting that data.  You can find out more by exploring the index below.
+
+### Reading data
+A simple example of how to use Objx:
+
+    // Use MustFromJSON to make an objx.Map from some JSON
+    m := objx.MustFromJSON(`{"name": "Mat", "age": 30}`)
+
+    // Get the details
+    name := m.Get("name").Str()
+    age := m.Get("age").Int()
+
+    // Get their nickname (or use their name if they don't have one)
+    nickname := m.Get("nickname").Str(name)
+
+### Ranging
+Since `objx.Map` is a `map[string]interface{}` you can treat it as such.  For example, to `range` the data, do what you would expect:
+
+    m := objx.MustFromJSON(json)
+    for key, value := range m {
+      // Your code...
+    }
+
+## Installation
+To install Objx, use go get:
+
+    go get github.com/stretchr/objx
+
+### Staying up to date
+To update Objx to the latest version, run:
+
+    go get -u github.com/stretchr/objx
+
+### Supported go versions
+We support the lastest two major Go versions, which are 1.8 and 1.9 at the moment.
+
+## Contributing
+Please feel free to submit issues, fork the repository and send pull requests!
diff --git a/vendor/github.com/stretchr/objx/Taskfile.yml b/vendor/github.com/stretchr/objx/Taskfile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..403b5f06ee458e1379f4d7ec99529fb5e4d40107
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/Taskfile.yml
@@ -0,0 +1,26 @@
+default:
+  deps: [test]
+
+dl-deps:
+  desc: Downloads cli dependencies
+  cmds:
+    - go get -u github.com/golang/lint/golint
+    - go get -u github.com/golang/dep/cmd/dep
+
+update-deps:
+  desc: Updates dependencies
+  cmds:
+    - dep ensure
+    - dep ensure -update
+    - dep prune
+
+lint:
+  desc: Runs golint
+  cmds:
+    - golint $(ls *.go | grep -v "doc.go")
+  silent: true
+
+test:
+  desc: Runs go tests
+  cmds:
+    - go test -race  .
diff --git a/vendor/github.com/stretchr/objx/accessors.go b/vendor/github.com/stretchr/objx/accessors.go
new file mode 100644
index 0000000000000000000000000000000000000000..d95be0ca9ec217c6a807fade77187a9162cce384
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/accessors.go
@@ -0,0 +1,171 @@
+package objx
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+// arrayAccesRegexString is the regex used to extract the array number
+// from the access path
+const arrayAccesRegexString = `^(.+)\[([0-9]+)\]$`
+
+// arrayAccesRegex is the compiled arrayAccesRegexString
+var arrayAccesRegex = regexp.MustCompile(arrayAccesRegexString)
+
+// Get gets the value using the specified selector and
+// returns it inside a new Obj object.
+//
+// If it cannot find the value, Get will return a nil
+// value inside an instance of Obj.
+//
+// Get can only operate directly on map[string]interface{} and []interface.
+//
+// Example
+//
+// To access the title of the third chapter of the second book, do:
+//
+//    o.Get("books[1].chapters[2].title")
+func (m Map) Get(selector string) *Value {
+	rawObj := access(m, selector, nil, false, false)
+	return &Value{data: rawObj}
+}
+
+// Set sets the value using the specified selector and
+// returns the object on which Set was called.
+//
+// Set can only operate directly on map[string]interface{} and []interface
+//
+// Example
+//
+// To set the title of the third chapter of the second book, do:
+//
+//    o.Set("books[1].chapters[2].title","Time to Go")
+func (m Map) Set(selector string, value interface{}) Map {
+	access(m, selector, value, true, false)
+	return m
+}
+
+// access accesses the object using the selector and performs the
+// appropriate action.
+func access(current, selector, value interface{}, isSet, panics bool) interface{} {
+
+	switch selector.(type) {
+	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
+
+		if array, ok := current.([]interface{}); ok {
+			index := intFromInterface(selector)
+
+			if index >= len(array) {
+				if panics {
+					panic(fmt.Sprintf("objx: Index %d is out of range. Slice only contains %d items.", index, len(array)))
+				}
+				return nil
+			}
+
+			return array[index]
+		}
+
+		return nil
+
+	case string:
+
+		selStr := selector.(string)
+		selSegs := strings.SplitN(selStr, PathSeparator, 2)
+		thisSel := selSegs[0]
+		index := -1
+		var err error
+
+		if strings.Contains(thisSel, "[") {
+			arrayMatches := arrayAccesRegex.FindStringSubmatch(thisSel)
+
+			if len(arrayMatches) > 0 {
+				// Get the key into the map
+				thisSel = arrayMatches[1]
+
+				// Get the index into the array at the key
+				index, err = strconv.Atoi(arrayMatches[2])
+
+				if err != nil {
+					// This should never happen. If it does, something has gone
+					// seriously wrong. Panic.
+					panic("objx: Array index is not an integer.  Must use array[int].")
+				}
+			}
+		}
+
+		if curMap, ok := current.(Map); ok {
+			current = map[string]interface{}(curMap)
+		}
+
+		// get the object in question
+		switch current.(type) {
+		case map[string]interface{}:
+			curMSI := current.(map[string]interface{})
+			if len(selSegs) <= 1 && isSet {
+				curMSI[thisSel] = value
+				return nil
+			}
+			current = curMSI[thisSel]
+		default:
+			current = nil
+		}
+
+		if current == nil && panics {
+			panic(fmt.Sprintf("objx: '%v' invalid on object.", selector))
+		}
+
+		// do we need to access the item of an array?
+		if index > -1 {
+			if array, ok := current.([]interface{}); ok {
+				if index < len(array) {
+					current = array[index]
+				} else {
+					if panics {
+						panic(fmt.Sprintf("objx: Index %d is out of range. Slice only contains %d items.", index, len(array)))
+					}
+					current = nil
+				}
+			}
+		}
+
+		if len(selSegs) > 1 {
+			current = access(current, selSegs[1], value, isSet, panics)
+		}
+
+	}
+	return current
+}
+
+// intFromInterface converts an interface object to the largest
+// representation of an unsigned integer using a type switch and
+// assertions
+func intFromInterface(selector interface{}) int {
+	var value int
+	switch selector.(type) {
+	case int:
+		value = selector.(int)
+	case int8:
+		value = int(selector.(int8))
+	case int16:
+		value = int(selector.(int16))
+	case int32:
+		value = int(selector.(int32))
+	case int64:
+		value = int(selector.(int64))
+	case uint:
+		value = int(selector.(uint))
+	case uint8:
+		value = int(selector.(uint8))
+	case uint16:
+		value = int(selector.(uint16))
+	case uint32:
+		value = int(selector.(uint32))
+	case uint64:
+		value = int(selector.(uint64))
+	default:
+		panic("objx: array access argument is not an integer type (this should never happen)")
+	}
+	return value
+}
diff --git a/vendor/github.com/stretchr/objx/constants.go b/vendor/github.com/stretchr/objx/constants.go
new file mode 100644
index 0000000000000000000000000000000000000000..f9eb42a25e0b92a7a2b308a5046b9b71fd1cac9b
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/constants.go
@@ -0,0 +1,13 @@
+package objx
+
+const (
+	// PathSeparator is the character used to separate the elements
+	// of the keypath.
+	//
+	// For example, `location.address.city`
+	PathSeparator string = "."
+
+	// SignatureSeparator is the character that is used to
+	// separate the Base64 string from the security signature.
+	SignatureSeparator = "_"
+)
diff --git a/vendor/github.com/stretchr/objx/conversions.go b/vendor/github.com/stretchr/objx/conversions.go
new file mode 100644
index 0000000000000000000000000000000000000000..5e020f310adc9aef33918db48ef051d82a83858c
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/conversions.go
@@ -0,0 +1,108 @@
+package objx
+
+import (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/url"
+)
+
+// JSON converts the contained object to a JSON string
+// representation
+func (m Map) JSON() (string, error) {
+	result, err := json.Marshal(m)
+	if err != nil {
+		err = errors.New("objx: JSON encode failed with: " + err.Error())
+	}
+	return string(result), err
+}
+
+// MustJSON converts the contained object to a JSON string
+// representation and panics if there is an error
+func (m Map) MustJSON() string {
+	result, err := m.JSON()
+	if err != nil {
+		panic(err.Error())
+	}
+	return result
+}
+
+// Base64 converts the contained object to a Base64 string
+// representation of the JSON string representation
+func (m Map) Base64() (string, error) {
+	var buf bytes.Buffer
+
+	jsonData, err := m.JSON()
+	if err != nil {
+		return "", err
+	}
+
+	encoder := base64.NewEncoder(base64.StdEncoding, &buf)
+	_, err = encoder.Write([]byte(jsonData))
+	if err != nil {
+		return "", err
+	}
+	_ = encoder.Close()
+
+	return buf.String(), nil
+}
+
+// MustBase64 converts the contained object to a Base64 string
+// representation of the JSON string representation and panics
+// if there is an error
+func (m Map) MustBase64() string {
+	result, err := m.Base64()
+	if err != nil {
+		panic(err.Error())
+	}
+	return result
+}
+
+// SignedBase64 converts the contained object to a Base64 string
+// representation of the JSON string representation and signs it
+// using the provided key.
+func (m Map) SignedBase64(key string) (string, error) {
+	base64, err := m.Base64()
+	if err != nil {
+		return "", err
+	}
+
+	sig := HashWithKey(base64, key)
+	return base64 + SignatureSeparator + sig, nil
+}
+
+// MustSignedBase64 converts the contained object to a Base64 string
+// representation of the JSON string representation and signs it
+// using the provided key and panics if there is an error
+func (m Map) MustSignedBase64(key string) string {
+	result, err := m.SignedBase64(key)
+	if err != nil {
+		panic(err.Error())
+	}
+	return result
+}
+
+/*
+	URL Query
+	------------------------------------------------
+*/
+
+// URLValues creates a url.Values object from an Obj. This
+// function requires that the wrapped object be a map[string]interface{}
+func (m Map) URLValues() url.Values {
+	vals := make(url.Values)
+	for k, v := range m {
+		//TODO: can this be done without sprintf?
+		vals.Set(k, fmt.Sprintf("%v", v))
+	}
+	return vals
+}
+
+// URLQuery gets an encoded URL query representing the given
+// Obj. This function requires that the wrapped object be a
+// map[string]interface{}
+func (m Map) URLQuery() (string, error) {
+	return m.URLValues().Encode(), nil
+}
diff --git a/vendor/github.com/stretchr/objx/doc.go b/vendor/github.com/stretchr/objx/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..6d6af1a83abf0c816be79b131271c1f311d28613
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/doc.go
@@ -0,0 +1,66 @@
+/*
+Objx - Go package for dealing with maps, slices, JSON and other data.
+
+Overview
+
+Objx provides the `objx.Map` type, which is a `map[string]interface{}` that exposes
+a powerful `Get` method (among others) that allows you to easily and quickly get
+access to data within the map, without having to worry too much about type assertions,
+missing data, default values etc.
+
+Pattern
+
+Objx uses a preditable pattern to make access data from within `map[string]interface{}` easy.
+Call one of the `objx.` functions to create your `objx.Map` to get going:
+
+    m, err := objx.FromJSON(json)
+
+NOTE: Any methods or functions with the `Must` prefix will panic if something goes wrong,
+the rest will be optimistic and try to figure things out without panicking.
+
+Use `Get` to access the value you're interested in.  You can use dot and array
+notation too:
+
+     m.Get("places[0].latlng")
+
+Once you have sought the `Value` you're interested in, you can use the `Is*` methods to determine its type.
+
+     if m.Get("code").IsStr() { // Your code... }
+
+Or you can just assume the type, and use one of the strong type methods to extract the real value:
+
+   m.Get("code").Int()
+
+If there's no value there (or if it's the wrong type) then a default value will be returned,
+or you can be explicit about the default value.
+
+     Get("code").Int(-1)
+
+If you're dealing with a slice of data as a value, Objx provides many useful methods for iterating,
+manipulating and selecting that data.  You can find out more by exploring the index below.
+
+Reading data
+
+A simple example of how to use Objx:
+
+   // Use MustFromJSON to make an objx.Map from some JSON
+   m := objx.MustFromJSON(`{"name": "Mat", "age": 30}`)
+
+   // Get the details
+   name := m.Get("name").Str()
+   age := m.Get("age").Int()
+
+   // Get their nickname (or use their name if they don't have one)
+   nickname := m.Get("nickname").Str(name)
+
+Ranging
+
+Since `objx.Map` is a `map[string]interface{}` you can treat it as such.
+For example, to `range` the data, do what you would expect:
+
+    m := objx.MustFromJSON(json)
+    for key, value := range m {
+      // Your code...
+    }
+*/
+package objx
diff --git a/vendor/github.com/stretchr/objx/map.go b/vendor/github.com/stretchr/objx/map.go
new file mode 100644
index 0000000000000000000000000000000000000000..7e9389a20a2afa25ecb936f7afde34871d643a07
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/map.go
@@ -0,0 +1,193 @@
+package objx
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"net/url"
+	"strings"
+)
+
+// MSIConvertable is an interface that defines methods for converting your
+// custom types to a map[string]interface{} representation.
+type MSIConvertable interface {
+	// MSI gets a map[string]interface{} (msi) representing the
+	// object.
+	MSI() map[string]interface{}
+}
+
+// Map provides extended functionality for working with
+// untyped data, in particular map[string]interface (msi).
+type Map map[string]interface{}
+
+// Value returns the internal value instance
+func (m Map) Value() *Value {
+	return &Value{data: m}
+}
+
+// Nil represents a nil Map.
+var Nil = New(nil)
+
+// New creates a new Map containing the map[string]interface{} in the data argument.
+// If the data argument is not a map[string]interface, New attempts to call the
+// MSI() method on the MSIConvertable interface to create one.
+func New(data interface{}) Map {
+	if _, ok := data.(map[string]interface{}); !ok {
+		if converter, ok := data.(MSIConvertable); ok {
+			data = converter.MSI()
+		} else {
+			return nil
+		}
+	}
+	return Map(data.(map[string]interface{}))
+}
+
+// MSI creates a map[string]interface{} and puts it inside a new Map.
+//
+// The arguments follow a key, value pattern.
+//
+// Panics
+//
+// Panics if any key argument is non-string or if there are an odd number of arguments.
+//
+// Example
+//
+// To easily create Maps:
+//
+//     m := objx.MSI("name", "Mat", "age", 29, "subobj", objx.MSI("active", true))
+//
+//     // creates an Map equivalent to
+//     m := objx.New(map[string]interface{}{"name": "Mat", "age": 29, "subobj": map[string]interface{}{"active": true}})
+func MSI(keyAndValuePairs ...interface{}) Map {
+	newMap := make(map[string]interface{})
+	keyAndValuePairsLen := len(keyAndValuePairs)
+	if keyAndValuePairsLen%2 != 0 {
+		panic("objx: MSI must have an even number of arguments following the 'key, value' pattern.")
+	}
+
+	for i := 0; i < keyAndValuePairsLen; i = i + 2 {
+		key := keyAndValuePairs[i]
+		value := keyAndValuePairs[i+1]
+
+		// make sure the key is a string
+		keyString, keyStringOK := key.(string)
+		if !keyStringOK {
+			panic("objx: MSI must follow 'string, interface{}' pattern.  " + keyString + " is not a valid key.")
+		}
+		newMap[keyString] = value
+	}
+	return New(newMap)
+}
+
+// ****** Conversion Constructors
+
+// MustFromJSON creates a new Map containing the data specified in the
+// jsonString.
+//
+// Panics if the JSON is invalid.
+func MustFromJSON(jsonString string) Map {
+	o, err := FromJSON(jsonString)
+	if err != nil {
+		panic("objx: MustFromJSON failed with error: " + err.Error())
+	}
+	return o
+}
+
+// FromJSON creates a new Map containing the data specified in the
+// jsonString.
+//
+// Returns an error if the JSON is invalid.
+func FromJSON(jsonString string) (Map, error) {
+	var data interface{}
+	err := json.Unmarshal([]byte(jsonString), &data)
+	if err != nil {
+		return Nil, err
+	}
+	return New(data), nil
+}
+
+// FromBase64 creates a new Obj containing the data specified
+// in the Base64 string.
+//
+// The string is an encoded JSON string returned by Base64
+func FromBase64(base64String string) (Map, error) {
+	decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64String))
+	decoded, err := ioutil.ReadAll(decoder)
+	if err != nil {
+		return nil, err
+	}
+	return FromJSON(string(decoded))
+}
+
+// MustFromBase64 creates a new Obj containing the data specified
+// in the Base64 string and panics if there is an error.
+//
+// The string is an encoded JSON string returned by Base64
+func MustFromBase64(base64String string) Map {
+	result, err := FromBase64(base64String)
+	if err != nil {
+		panic("objx: MustFromBase64 failed with error: " + err.Error())
+	}
+	return result
+}
+
+// FromSignedBase64 creates a new Obj containing the data specified
+// in the Base64 string.
+//
+// The string is an encoded JSON string returned by SignedBase64
+func FromSignedBase64(base64String, key string) (Map, error) {
+	parts := strings.Split(base64String, SignatureSeparator)
+	if len(parts) != 2 {
+		return nil, errors.New("objx: Signed base64 string is malformed")
+	}
+
+	sig := HashWithKey(parts[0], key)
+	if parts[1] != sig {
+		return nil, errors.New("objx: Signature for base64 data does not match")
+	}
+	return FromBase64(parts[0])
+}
+
+// MustFromSignedBase64 creates a new Obj containing the data specified
+// in the Base64 string and panics if there is an error.
+//
+// The string is an encoded JSON string returned by Base64
+func MustFromSignedBase64(base64String, key string) Map {
+	result, err := FromSignedBase64(base64String, key)
+	if err != nil {
+		panic("objx: MustFromSignedBase64 failed with error: " + err.Error())
+	}
+	return result
+}
+
+// FromURLQuery generates a new Obj by parsing the specified
+// query.
+//
+// For queries with multiple values, the first value is selected.
+func FromURLQuery(query string) (Map, error) {
+	vals, err := url.ParseQuery(query)
+	if err != nil {
+		return nil, err
+	}
+
+	m := make(map[string]interface{})
+	for k, vals := range vals {
+		m[k] = vals[0]
+	}
+	return New(m), nil
+}
+
+// MustFromURLQuery generates a new Obj by parsing the specified
+// query.
+//
+// For queries with multiple values, the first value is selected.
+//
+// Panics if it encounters an error
+func MustFromURLQuery(query string) Map {
+	o, err := FromURLQuery(query)
+	if err != nil {
+		panic("objx: MustFromURLQuery failed with error: " + err.Error())
+	}
+	return o
+}
diff --git a/vendor/github.com/stretchr/objx/mutations.go b/vendor/github.com/stretchr/objx/mutations.go
new file mode 100644
index 0000000000000000000000000000000000000000..e7b8eb794c8c4edc7aab385266178b559dfcf18f
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/mutations.go
@@ -0,0 +1,74 @@
+package objx
+
+// Exclude returns a new Map with the keys in the specified []string
+// excluded.
+func (m Map) Exclude(exclude []string) Map {
+	excluded := make(Map)
+	for k, v := range m {
+		var shouldInclude = true
+		for _, toExclude := range exclude {
+			if k == toExclude {
+				shouldInclude = false
+				break
+			}
+		}
+		if shouldInclude {
+			excluded[k] = v
+		}
+	}
+	return excluded
+}
+
+// Copy creates a shallow copy of the Obj.
+func (m Map) Copy() Map {
+	copied := make(map[string]interface{})
+	for k, v := range m {
+		copied[k] = v
+	}
+	return New(copied)
+}
+
+// Merge blends the specified map with a copy of this map and returns the result.
+//
+// Keys that appear in both will be selected from the specified map.
+// This method requires that the wrapped object be a map[string]interface{}
+func (m Map) Merge(merge Map) Map {
+	return m.Copy().MergeHere(merge)
+}
+
+// MergeHere blends the specified map with this map and returns the current map.
+//
+// Keys that appear in both will be selected from the specified map. The original map
+// will be modified. This method requires that
+// the wrapped object be a map[string]interface{}
+func (m Map) MergeHere(merge Map) Map {
+	for k, v := range merge {
+		m[k] = v
+	}
+	return m
+}
+
+// Transform builds a new Obj giving the transformer a chance
+// to change the keys and values as it goes. This method requires that
+// the wrapped object be a map[string]interface{}
+func (m Map) Transform(transformer func(key string, value interface{}) (string, interface{})) Map {
+	newMap := make(map[string]interface{})
+	for k, v := range m {
+		modifiedKey, modifiedVal := transformer(k, v)
+		newMap[modifiedKey] = modifiedVal
+	}
+	return New(newMap)
+}
+
+// TransformKeys builds a new map using the specified key mapping.
+//
+// Unspecified keys will be unaltered.
+// This method requires that the wrapped object be a map[string]interface{}
+func (m Map) TransformKeys(mapping map[string]string) Map {
+	return m.Transform(func(key string, value interface{}) (string, interface{}) {
+		if newKey, ok := mapping[key]; ok {
+			return newKey, value
+		}
+		return key, value
+	})
+}
diff --git a/vendor/github.com/stretchr/objx/security.go b/vendor/github.com/stretchr/objx/security.go
new file mode 100644
index 0000000000000000000000000000000000000000..e052ff890c163e10f27929efcc22bd4b7fd8a53e
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/security.go
@@ -0,0 +1,17 @@
+package objx
+
+import (
+	"crypto/sha1"
+	"encoding/hex"
+)
+
+// HashWithKey hashes the specified string using the security
+// key.
+func HashWithKey(data, key string) string {
+	hash := sha1.New()
+	_, err := hash.Write([]byte(data + ":" + key))
+	if err != nil {
+		return ""
+	}
+	return hex.EncodeToString(hash.Sum(nil))
+}
diff --git a/vendor/github.com/stretchr/objx/tests.go b/vendor/github.com/stretchr/objx/tests.go
new file mode 100644
index 0000000000000000000000000000000000000000..d9e0b479a4c0f89957ed7cebc98d186740d1a96c
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/tests.go
@@ -0,0 +1,17 @@
+package objx
+
+// Has gets whether there is something at the specified selector
+// or not.
+//
+// If m is nil, Has will always return false.
+func (m Map) Has(selector string) bool {
+	if m == nil {
+		return false
+	}
+	return !m.Get(selector).IsNil()
+}
+
+// IsNil gets whether the data is nil or not.
+func (v *Value) IsNil() bool {
+	return v == nil || v.data == nil
+}
diff --git a/vendor/github.com/stretchr/objx/type_specific_codegen.go b/vendor/github.com/stretchr/objx/type_specific_codegen.go
new file mode 100644
index 0000000000000000000000000000000000000000..202a91f8c199f9d06f1962408ec9c4534b19e773
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/type_specific_codegen.go
@@ -0,0 +1,2501 @@
+package objx
+
+/*
+	Inter (interface{} and []interface{})
+*/
+
+// Inter gets the value as a interface{}, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Inter(optionalDefault ...interface{}) interface{} {
+	if s, ok := v.data.(interface{}); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustInter gets the value as a interface{}.
+//
+// Panics if the object is not a interface{}.
+func (v *Value) MustInter() interface{} {
+	return v.data.(interface{})
+}
+
+// InterSlice gets the value as a []interface{}, returns the optionalDefault
+// value or nil if the value is not a []interface{}.
+func (v *Value) InterSlice(optionalDefault ...[]interface{}) []interface{} {
+	if s, ok := v.data.([]interface{}); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustInterSlice gets the value as a []interface{}.
+//
+// Panics if the object is not a []interface{}.
+func (v *Value) MustInterSlice() []interface{} {
+	return v.data.([]interface{})
+}
+
+// IsInter gets whether the object contained is a interface{} or not.
+func (v *Value) IsInter() bool {
+	_, ok := v.data.(interface{})
+	return ok
+}
+
+// IsInterSlice gets whether the object contained is a []interface{} or not.
+func (v *Value) IsInterSlice() bool {
+	_, ok := v.data.([]interface{})
+	return ok
+}
+
+// EachInter calls the specified callback for each object
+// in the []interface{}.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachInter(callback func(int, interface{}) bool) *Value {
+	for index, val := range v.MustInterSlice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereInter uses the specified decider function to select items
+// from the []interface{}.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereInter(decider func(int, interface{}) bool) *Value {
+	var selected []interface{}
+	v.EachInter(func(index int, val interface{}) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupInter uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]interface{}.
+func (v *Value) GroupInter(grouper func(int, interface{}) string) *Value {
+	groups := make(map[string][]interface{})
+	v.EachInter(func(index int, val interface{}) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]interface{}, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceInter uses the specified function to replace each interface{}s
+// by iterating each item.  The data in the returned result will be a
+// []interface{} containing the replaced items.
+func (v *Value) ReplaceInter(replacer func(int, interface{}) interface{}) *Value {
+	arr := v.MustInterSlice()
+	replaced := make([]interface{}, len(arr))
+	v.EachInter(func(index int, val interface{}) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectInter uses the specified collector function to collect a value
+// for each of the interface{}s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectInter(collector func(int, interface{}) interface{}) *Value {
+	arr := v.MustInterSlice()
+	collected := make([]interface{}, len(arr))
+	v.EachInter(func(index int, val interface{}) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	MSI (map[string]interface{} and []map[string]interface{})
+*/
+
+// MSI gets the value as a map[string]interface{}, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) MSI(optionalDefault ...map[string]interface{}) map[string]interface{} {
+	if s, ok := v.data.(map[string]interface{}); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustMSI gets the value as a map[string]interface{}.
+//
+// Panics if the object is not a map[string]interface{}.
+func (v *Value) MustMSI() map[string]interface{} {
+	return v.data.(map[string]interface{})
+}
+
+// MSISlice gets the value as a []map[string]interface{}, returns the optionalDefault
+// value or nil if the value is not a []map[string]interface{}.
+func (v *Value) MSISlice(optionalDefault ...[]map[string]interface{}) []map[string]interface{} {
+	if s, ok := v.data.([]map[string]interface{}); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustMSISlice gets the value as a []map[string]interface{}.
+//
+// Panics if the object is not a []map[string]interface{}.
+func (v *Value) MustMSISlice() []map[string]interface{} {
+	return v.data.([]map[string]interface{})
+}
+
+// IsMSI gets whether the object contained is a map[string]interface{} or not.
+func (v *Value) IsMSI() bool {
+	_, ok := v.data.(map[string]interface{})
+	return ok
+}
+
+// IsMSISlice gets whether the object contained is a []map[string]interface{} or not.
+func (v *Value) IsMSISlice() bool {
+	_, ok := v.data.([]map[string]interface{})
+	return ok
+}
+
+// EachMSI calls the specified callback for each object
+// in the []map[string]interface{}.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachMSI(callback func(int, map[string]interface{}) bool) *Value {
+	for index, val := range v.MustMSISlice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereMSI uses the specified decider function to select items
+// from the []map[string]interface{}.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereMSI(decider func(int, map[string]interface{}) bool) *Value {
+	var selected []map[string]interface{}
+	v.EachMSI(func(index int, val map[string]interface{}) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupMSI uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]map[string]interface{}.
+func (v *Value) GroupMSI(grouper func(int, map[string]interface{}) string) *Value {
+	groups := make(map[string][]map[string]interface{})
+	v.EachMSI(func(index int, val map[string]interface{}) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]map[string]interface{}, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceMSI uses the specified function to replace each map[string]interface{}s
+// by iterating each item.  The data in the returned result will be a
+// []map[string]interface{} containing the replaced items.
+func (v *Value) ReplaceMSI(replacer func(int, map[string]interface{}) map[string]interface{}) *Value {
+	arr := v.MustMSISlice()
+	replaced := make([]map[string]interface{}, len(arr))
+	v.EachMSI(func(index int, val map[string]interface{}) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectMSI uses the specified collector function to collect a value
+// for each of the map[string]interface{}s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectMSI(collector func(int, map[string]interface{}) interface{}) *Value {
+	arr := v.MustMSISlice()
+	collected := make([]interface{}, len(arr))
+	v.EachMSI(func(index int, val map[string]interface{}) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	ObjxMap ((Map) and [](Map))
+*/
+
+// ObjxMap gets the value as a (Map), returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) ObjxMap(optionalDefault ...(Map)) Map {
+	if s, ok := v.data.((Map)); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return New(nil)
+}
+
+// MustObjxMap gets the value as a (Map).
+//
+// Panics if the object is not a (Map).
+func (v *Value) MustObjxMap() Map {
+	return v.data.((Map))
+}
+
+// ObjxMapSlice gets the value as a [](Map), returns the optionalDefault
+// value or nil if the value is not a [](Map).
+func (v *Value) ObjxMapSlice(optionalDefault ...[](Map)) [](Map) {
+	if s, ok := v.data.([](Map)); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustObjxMapSlice gets the value as a [](Map).
+//
+// Panics if the object is not a [](Map).
+func (v *Value) MustObjxMapSlice() [](Map) {
+	return v.data.([](Map))
+}
+
+// IsObjxMap gets whether the object contained is a (Map) or not.
+func (v *Value) IsObjxMap() bool {
+	_, ok := v.data.((Map))
+	return ok
+}
+
+// IsObjxMapSlice gets whether the object contained is a [](Map) or not.
+func (v *Value) IsObjxMapSlice() bool {
+	_, ok := v.data.([](Map))
+	return ok
+}
+
+// EachObjxMap calls the specified callback for each object
+// in the [](Map).
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachObjxMap(callback func(int, Map) bool) *Value {
+	for index, val := range v.MustObjxMapSlice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereObjxMap uses the specified decider function to select items
+// from the [](Map).  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereObjxMap(decider func(int, Map) bool) *Value {
+	var selected [](Map)
+	v.EachObjxMap(func(index int, val Map) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupObjxMap uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][](Map).
+func (v *Value) GroupObjxMap(grouper func(int, Map) string) *Value {
+	groups := make(map[string][](Map))
+	v.EachObjxMap(func(index int, val Map) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([](Map), 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceObjxMap uses the specified function to replace each (Map)s
+// by iterating each item.  The data in the returned result will be a
+// [](Map) containing the replaced items.
+func (v *Value) ReplaceObjxMap(replacer func(int, Map) Map) *Value {
+	arr := v.MustObjxMapSlice()
+	replaced := make([](Map), len(arr))
+	v.EachObjxMap(func(index int, val Map) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectObjxMap uses the specified collector function to collect a value
+// for each of the (Map)s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectObjxMap(collector func(int, Map) interface{}) *Value {
+	arr := v.MustObjxMapSlice()
+	collected := make([]interface{}, len(arr))
+	v.EachObjxMap(func(index int, val Map) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Bool (bool and []bool)
+*/
+
+// Bool gets the value as a bool, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Bool(optionalDefault ...bool) bool {
+	if s, ok := v.data.(bool); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return false
+}
+
+// MustBool gets the value as a bool.
+//
+// Panics if the object is not a bool.
+func (v *Value) MustBool() bool {
+	return v.data.(bool)
+}
+
+// BoolSlice gets the value as a []bool, returns the optionalDefault
+// value or nil if the value is not a []bool.
+func (v *Value) BoolSlice(optionalDefault ...[]bool) []bool {
+	if s, ok := v.data.([]bool); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustBoolSlice gets the value as a []bool.
+//
+// Panics if the object is not a []bool.
+func (v *Value) MustBoolSlice() []bool {
+	return v.data.([]bool)
+}
+
+// IsBool gets whether the object contained is a bool or not.
+func (v *Value) IsBool() bool {
+	_, ok := v.data.(bool)
+	return ok
+}
+
+// IsBoolSlice gets whether the object contained is a []bool or not.
+func (v *Value) IsBoolSlice() bool {
+	_, ok := v.data.([]bool)
+	return ok
+}
+
+// EachBool calls the specified callback for each object
+// in the []bool.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachBool(callback func(int, bool) bool) *Value {
+	for index, val := range v.MustBoolSlice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereBool uses the specified decider function to select items
+// from the []bool.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereBool(decider func(int, bool) bool) *Value {
+	var selected []bool
+	v.EachBool(func(index int, val bool) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupBool uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]bool.
+func (v *Value) GroupBool(grouper func(int, bool) string) *Value {
+	groups := make(map[string][]bool)
+	v.EachBool(func(index int, val bool) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]bool, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceBool uses the specified function to replace each bools
+// by iterating each item.  The data in the returned result will be a
+// []bool containing the replaced items.
+func (v *Value) ReplaceBool(replacer func(int, bool) bool) *Value {
+	arr := v.MustBoolSlice()
+	replaced := make([]bool, len(arr))
+	v.EachBool(func(index int, val bool) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectBool uses the specified collector function to collect a value
+// for each of the bools in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectBool(collector func(int, bool) interface{}) *Value {
+	arr := v.MustBoolSlice()
+	collected := make([]interface{}, len(arr))
+	v.EachBool(func(index int, val bool) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Str (string and []string)
+*/
+
+// Str gets the value as a string, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Str(optionalDefault ...string) string {
+	if s, ok := v.data.(string); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return ""
+}
+
+// MustStr gets the value as a string.
+//
+// Panics if the object is not a string.
+func (v *Value) MustStr() string {
+	return v.data.(string)
+}
+
+// StrSlice gets the value as a []string, returns the optionalDefault
+// value or nil if the value is not a []string.
+func (v *Value) StrSlice(optionalDefault ...[]string) []string {
+	if s, ok := v.data.([]string); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustStrSlice gets the value as a []string.
+//
+// Panics if the object is not a []string.
+func (v *Value) MustStrSlice() []string {
+	return v.data.([]string)
+}
+
+// IsStr gets whether the object contained is a string or not.
+func (v *Value) IsStr() bool {
+	_, ok := v.data.(string)
+	return ok
+}
+
+// IsStrSlice gets whether the object contained is a []string or not.
+func (v *Value) IsStrSlice() bool {
+	_, ok := v.data.([]string)
+	return ok
+}
+
+// EachStr calls the specified callback for each object
+// in the []string.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachStr(callback func(int, string) bool) *Value {
+	for index, val := range v.MustStrSlice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereStr uses the specified decider function to select items
+// from the []string.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereStr(decider func(int, string) bool) *Value {
+	var selected []string
+	v.EachStr(func(index int, val string) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupStr uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]string.
+func (v *Value) GroupStr(grouper func(int, string) string) *Value {
+	groups := make(map[string][]string)
+	v.EachStr(func(index int, val string) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]string, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceStr uses the specified function to replace each strings
+// by iterating each item.  The data in the returned result will be a
+// []string containing the replaced items.
+func (v *Value) ReplaceStr(replacer func(int, string) string) *Value {
+	arr := v.MustStrSlice()
+	replaced := make([]string, len(arr))
+	v.EachStr(func(index int, val string) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectStr uses the specified collector function to collect a value
+// for each of the strings in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectStr(collector func(int, string) interface{}) *Value {
+	arr := v.MustStrSlice()
+	collected := make([]interface{}, len(arr))
+	v.EachStr(func(index int, val string) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Int (int and []int)
+*/
+
+// Int gets the value as a int, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Int(optionalDefault ...int) int {
+	if s, ok := v.data.(int); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustInt gets the value as a int.
+//
+// Panics if the object is not a int.
+func (v *Value) MustInt() int {
+	return v.data.(int)
+}
+
+// IntSlice gets the value as a []int, returns the optionalDefault
+// value or nil if the value is not a []int.
+func (v *Value) IntSlice(optionalDefault ...[]int) []int {
+	if s, ok := v.data.([]int); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustIntSlice gets the value as a []int.
+//
+// Panics if the object is not a []int.
+func (v *Value) MustIntSlice() []int {
+	return v.data.([]int)
+}
+
+// IsInt gets whether the object contained is a int or not.
+func (v *Value) IsInt() bool {
+	_, ok := v.data.(int)
+	return ok
+}
+
+// IsIntSlice gets whether the object contained is a []int or not.
+func (v *Value) IsIntSlice() bool {
+	_, ok := v.data.([]int)
+	return ok
+}
+
+// EachInt calls the specified callback for each object
+// in the []int.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachInt(callback func(int, int) bool) *Value {
+	for index, val := range v.MustIntSlice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereInt uses the specified decider function to select items
+// from the []int.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereInt(decider func(int, int) bool) *Value {
+	var selected []int
+	v.EachInt(func(index int, val int) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupInt uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]int.
+func (v *Value) GroupInt(grouper func(int, int) string) *Value {
+	groups := make(map[string][]int)
+	v.EachInt(func(index int, val int) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]int, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceInt uses the specified function to replace each ints
+// by iterating each item.  The data in the returned result will be a
+// []int containing the replaced items.
+func (v *Value) ReplaceInt(replacer func(int, int) int) *Value {
+	arr := v.MustIntSlice()
+	replaced := make([]int, len(arr))
+	v.EachInt(func(index int, val int) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectInt uses the specified collector function to collect a value
+// for each of the ints in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectInt(collector func(int, int) interface{}) *Value {
+	arr := v.MustIntSlice()
+	collected := make([]interface{}, len(arr))
+	v.EachInt(func(index int, val int) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Int8 (int8 and []int8)
+*/
+
+// Int8 gets the value as a int8, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Int8(optionalDefault ...int8) int8 {
+	if s, ok := v.data.(int8); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustInt8 gets the value as a int8.
+//
+// Panics if the object is not a int8.
+func (v *Value) MustInt8() int8 {
+	return v.data.(int8)
+}
+
+// Int8Slice gets the value as a []int8, returns the optionalDefault
+// value or nil if the value is not a []int8.
+func (v *Value) Int8Slice(optionalDefault ...[]int8) []int8 {
+	if s, ok := v.data.([]int8); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustInt8Slice gets the value as a []int8.
+//
+// Panics if the object is not a []int8.
+func (v *Value) MustInt8Slice() []int8 {
+	return v.data.([]int8)
+}
+
+// IsInt8 gets whether the object contained is a int8 or not.
+func (v *Value) IsInt8() bool {
+	_, ok := v.data.(int8)
+	return ok
+}
+
+// IsInt8Slice gets whether the object contained is a []int8 or not.
+func (v *Value) IsInt8Slice() bool {
+	_, ok := v.data.([]int8)
+	return ok
+}
+
+// EachInt8 calls the specified callback for each object
+// in the []int8.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachInt8(callback func(int, int8) bool) *Value {
+	for index, val := range v.MustInt8Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereInt8 uses the specified decider function to select items
+// from the []int8.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereInt8(decider func(int, int8) bool) *Value {
+	var selected []int8
+	v.EachInt8(func(index int, val int8) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupInt8 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]int8.
+func (v *Value) GroupInt8(grouper func(int, int8) string) *Value {
+	groups := make(map[string][]int8)
+	v.EachInt8(func(index int, val int8) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]int8, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceInt8 uses the specified function to replace each int8s
+// by iterating each item.  The data in the returned result will be a
+// []int8 containing the replaced items.
+func (v *Value) ReplaceInt8(replacer func(int, int8) int8) *Value {
+	arr := v.MustInt8Slice()
+	replaced := make([]int8, len(arr))
+	v.EachInt8(func(index int, val int8) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectInt8 uses the specified collector function to collect a value
+// for each of the int8s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectInt8(collector func(int, int8) interface{}) *Value {
+	arr := v.MustInt8Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachInt8(func(index int, val int8) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Int16 (int16 and []int16)
+*/
+
+// Int16 gets the value as a int16, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Int16(optionalDefault ...int16) int16 {
+	if s, ok := v.data.(int16); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustInt16 gets the value as a int16.
+//
+// Panics if the object is not a int16.
+func (v *Value) MustInt16() int16 {
+	return v.data.(int16)
+}
+
+// Int16Slice gets the value as a []int16, returns the optionalDefault
+// value or nil if the value is not a []int16.
+func (v *Value) Int16Slice(optionalDefault ...[]int16) []int16 {
+	if s, ok := v.data.([]int16); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustInt16Slice gets the value as a []int16.
+//
+// Panics if the object is not a []int16.
+func (v *Value) MustInt16Slice() []int16 {
+	return v.data.([]int16)
+}
+
+// IsInt16 gets whether the object contained is a int16 or not.
+func (v *Value) IsInt16() bool {
+	_, ok := v.data.(int16)
+	return ok
+}
+
+// IsInt16Slice gets whether the object contained is a []int16 or not.
+func (v *Value) IsInt16Slice() bool {
+	_, ok := v.data.([]int16)
+	return ok
+}
+
+// EachInt16 calls the specified callback for each object
+// in the []int16.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachInt16(callback func(int, int16) bool) *Value {
+	for index, val := range v.MustInt16Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereInt16 uses the specified decider function to select items
+// from the []int16.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereInt16(decider func(int, int16) bool) *Value {
+	var selected []int16
+	v.EachInt16(func(index int, val int16) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupInt16 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]int16.
+func (v *Value) GroupInt16(grouper func(int, int16) string) *Value {
+	groups := make(map[string][]int16)
+	v.EachInt16(func(index int, val int16) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]int16, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceInt16 uses the specified function to replace each int16s
+// by iterating each item.  The data in the returned result will be a
+// []int16 containing the replaced items.
+func (v *Value) ReplaceInt16(replacer func(int, int16) int16) *Value {
+	arr := v.MustInt16Slice()
+	replaced := make([]int16, len(arr))
+	v.EachInt16(func(index int, val int16) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectInt16 uses the specified collector function to collect a value
+// for each of the int16s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectInt16(collector func(int, int16) interface{}) *Value {
+	arr := v.MustInt16Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachInt16(func(index int, val int16) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Int32 (int32 and []int32)
+*/
+
+// Int32 gets the value as a int32, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Int32(optionalDefault ...int32) int32 {
+	if s, ok := v.data.(int32); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustInt32 gets the value as a int32.
+//
+// Panics if the object is not a int32.
+func (v *Value) MustInt32() int32 {
+	return v.data.(int32)
+}
+
+// Int32Slice gets the value as a []int32, returns the optionalDefault
+// value or nil if the value is not a []int32.
+func (v *Value) Int32Slice(optionalDefault ...[]int32) []int32 {
+	if s, ok := v.data.([]int32); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustInt32Slice gets the value as a []int32.
+//
+// Panics if the object is not a []int32.
+func (v *Value) MustInt32Slice() []int32 {
+	return v.data.([]int32)
+}
+
+// IsInt32 gets whether the object contained is a int32 or not.
+func (v *Value) IsInt32() bool {
+	_, ok := v.data.(int32)
+	return ok
+}
+
+// IsInt32Slice gets whether the object contained is a []int32 or not.
+func (v *Value) IsInt32Slice() bool {
+	_, ok := v.data.([]int32)
+	return ok
+}
+
+// EachInt32 calls the specified callback for each object
+// in the []int32.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachInt32(callback func(int, int32) bool) *Value {
+	for index, val := range v.MustInt32Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereInt32 uses the specified decider function to select items
+// from the []int32.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereInt32(decider func(int, int32) bool) *Value {
+	var selected []int32
+	v.EachInt32(func(index int, val int32) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupInt32 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]int32.
+func (v *Value) GroupInt32(grouper func(int, int32) string) *Value {
+	groups := make(map[string][]int32)
+	v.EachInt32(func(index int, val int32) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]int32, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceInt32 uses the specified function to replace each int32s
+// by iterating each item.  The data in the returned result will be a
+// []int32 containing the replaced items.
+func (v *Value) ReplaceInt32(replacer func(int, int32) int32) *Value {
+	arr := v.MustInt32Slice()
+	replaced := make([]int32, len(arr))
+	v.EachInt32(func(index int, val int32) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectInt32 uses the specified collector function to collect a value
+// for each of the int32s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectInt32(collector func(int, int32) interface{}) *Value {
+	arr := v.MustInt32Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachInt32(func(index int, val int32) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Int64 (int64 and []int64)
+*/
+
+// Int64 gets the value as a int64, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Int64(optionalDefault ...int64) int64 {
+	if s, ok := v.data.(int64); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustInt64 gets the value as a int64.
+//
+// Panics if the object is not a int64.
+func (v *Value) MustInt64() int64 {
+	return v.data.(int64)
+}
+
+// Int64Slice gets the value as a []int64, returns the optionalDefault
+// value or nil if the value is not a []int64.
+func (v *Value) Int64Slice(optionalDefault ...[]int64) []int64 {
+	if s, ok := v.data.([]int64); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustInt64Slice gets the value as a []int64.
+//
+// Panics if the object is not a []int64.
+func (v *Value) MustInt64Slice() []int64 {
+	return v.data.([]int64)
+}
+
+// IsInt64 gets whether the object contained is a int64 or not.
+func (v *Value) IsInt64() bool {
+	_, ok := v.data.(int64)
+	return ok
+}
+
+// IsInt64Slice gets whether the object contained is a []int64 or not.
+func (v *Value) IsInt64Slice() bool {
+	_, ok := v.data.([]int64)
+	return ok
+}
+
+// EachInt64 calls the specified callback for each object
+// in the []int64.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachInt64(callback func(int, int64) bool) *Value {
+	for index, val := range v.MustInt64Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereInt64 uses the specified decider function to select items
+// from the []int64.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereInt64(decider func(int, int64) bool) *Value {
+	var selected []int64
+	v.EachInt64(func(index int, val int64) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupInt64 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]int64.
+func (v *Value) GroupInt64(grouper func(int, int64) string) *Value {
+	groups := make(map[string][]int64)
+	v.EachInt64(func(index int, val int64) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]int64, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceInt64 uses the specified function to replace each int64s
+// by iterating each item.  The data in the returned result will be a
+// []int64 containing the replaced items.
+func (v *Value) ReplaceInt64(replacer func(int, int64) int64) *Value {
+	arr := v.MustInt64Slice()
+	replaced := make([]int64, len(arr))
+	v.EachInt64(func(index int, val int64) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectInt64 uses the specified collector function to collect a value
+// for each of the int64s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectInt64(collector func(int, int64) interface{}) *Value {
+	arr := v.MustInt64Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachInt64(func(index int, val int64) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Uint (uint and []uint)
+*/
+
+// Uint gets the value as a uint, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Uint(optionalDefault ...uint) uint {
+	if s, ok := v.data.(uint); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustUint gets the value as a uint.
+//
+// Panics if the object is not a uint.
+func (v *Value) MustUint() uint {
+	return v.data.(uint)
+}
+
+// UintSlice gets the value as a []uint, returns the optionalDefault
+// value or nil if the value is not a []uint.
+func (v *Value) UintSlice(optionalDefault ...[]uint) []uint {
+	if s, ok := v.data.([]uint); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustUintSlice gets the value as a []uint.
+//
+// Panics if the object is not a []uint.
+func (v *Value) MustUintSlice() []uint {
+	return v.data.([]uint)
+}
+
+// IsUint gets whether the object contained is a uint or not.
+func (v *Value) IsUint() bool {
+	_, ok := v.data.(uint)
+	return ok
+}
+
+// IsUintSlice gets whether the object contained is a []uint or not.
+func (v *Value) IsUintSlice() bool {
+	_, ok := v.data.([]uint)
+	return ok
+}
+
+// EachUint calls the specified callback for each object
+// in the []uint.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachUint(callback func(int, uint) bool) *Value {
+	for index, val := range v.MustUintSlice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereUint uses the specified decider function to select items
+// from the []uint.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereUint(decider func(int, uint) bool) *Value {
+	var selected []uint
+	v.EachUint(func(index int, val uint) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupUint uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]uint.
+func (v *Value) GroupUint(grouper func(int, uint) string) *Value {
+	groups := make(map[string][]uint)
+	v.EachUint(func(index int, val uint) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]uint, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceUint uses the specified function to replace each uints
+// by iterating each item.  The data in the returned result will be a
+// []uint containing the replaced items.
+func (v *Value) ReplaceUint(replacer func(int, uint) uint) *Value {
+	arr := v.MustUintSlice()
+	replaced := make([]uint, len(arr))
+	v.EachUint(func(index int, val uint) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectUint uses the specified collector function to collect a value
+// for each of the uints in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectUint(collector func(int, uint) interface{}) *Value {
+	arr := v.MustUintSlice()
+	collected := make([]interface{}, len(arr))
+	v.EachUint(func(index int, val uint) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Uint8 (uint8 and []uint8)
+*/
+
+// Uint8 gets the value as a uint8, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Uint8(optionalDefault ...uint8) uint8 {
+	if s, ok := v.data.(uint8); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustUint8 gets the value as a uint8.
+//
+// Panics if the object is not a uint8.
+func (v *Value) MustUint8() uint8 {
+	return v.data.(uint8)
+}
+
+// Uint8Slice gets the value as a []uint8, returns the optionalDefault
+// value or nil if the value is not a []uint8.
+func (v *Value) Uint8Slice(optionalDefault ...[]uint8) []uint8 {
+	if s, ok := v.data.([]uint8); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustUint8Slice gets the value as a []uint8.
+//
+// Panics if the object is not a []uint8.
+func (v *Value) MustUint8Slice() []uint8 {
+	return v.data.([]uint8)
+}
+
+// IsUint8 gets whether the object contained is a uint8 or not.
+func (v *Value) IsUint8() bool {
+	_, ok := v.data.(uint8)
+	return ok
+}
+
+// IsUint8Slice gets whether the object contained is a []uint8 or not.
+func (v *Value) IsUint8Slice() bool {
+	_, ok := v.data.([]uint8)
+	return ok
+}
+
+// EachUint8 calls the specified callback for each object
+// in the []uint8.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachUint8(callback func(int, uint8) bool) *Value {
+	for index, val := range v.MustUint8Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereUint8 uses the specified decider function to select items
+// from the []uint8.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereUint8(decider func(int, uint8) bool) *Value {
+	var selected []uint8
+	v.EachUint8(func(index int, val uint8) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupUint8 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]uint8.
+func (v *Value) GroupUint8(grouper func(int, uint8) string) *Value {
+	groups := make(map[string][]uint8)
+	v.EachUint8(func(index int, val uint8) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]uint8, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceUint8 uses the specified function to replace each uint8s
+// by iterating each item.  The data in the returned result will be a
+// []uint8 containing the replaced items.
+func (v *Value) ReplaceUint8(replacer func(int, uint8) uint8) *Value {
+	arr := v.MustUint8Slice()
+	replaced := make([]uint8, len(arr))
+	v.EachUint8(func(index int, val uint8) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectUint8 uses the specified collector function to collect a value
+// for each of the uint8s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectUint8(collector func(int, uint8) interface{}) *Value {
+	arr := v.MustUint8Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachUint8(func(index int, val uint8) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Uint16 (uint16 and []uint16)
+*/
+
+// Uint16 gets the value as a uint16, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Uint16(optionalDefault ...uint16) uint16 {
+	if s, ok := v.data.(uint16); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustUint16 gets the value as a uint16.
+//
+// Panics if the object is not a uint16.
+func (v *Value) MustUint16() uint16 {
+	return v.data.(uint16)
+}
+
+// Uint16Slice gets the value as a []uint16, returns the optionalDefault
+// value or nil if the value is not a []uint16.
+func (v *Value) Uint16Slice(optionalDefault ...[]uint16) []uint16 {
+	if s, ok := v.data.([]uint16); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustUint16Slice gets the value as a []uint16.
+//
+// Panics if the object is not a []uint16.
+func (v *Value) MustUint16Slice() []uint16 {
+	return v.data.([]uint16)
+}
+
+// IsUint16 gets whether the object contained is a uint16 or not.
+func (v *Value) IsUint16() bool {
+	_, ok := v.data.(uint16)
+	return ok
+}
+
+// IsUint16Slice gets whether the object contained is a []uint16 or not.
+func (v *Value) IsUint16Slice() bool {
+	_, ok := v.data.([]uint16)
+	return ok
+}
+
+// EachUint16 calls the specified callback for each object
+// in the []uint16.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachUint16(callback func(int, uint16) bool) *Value {
+	for index, val := range v.MustUint16Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereUint16 uses the specified decider function to select items
+// from the []uint16.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereUint16(decider func(int, uint16) bool) *Value {
+	var selected []uint16
+	v.EachUint16(func(index int, val uint16) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupUint16 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]uint16.
+func (v *Value) GroupUint16(grouper func(int, uint16) string) *Value {
+	groups := make(map[string][]uint16)
+	v.EachUint16(func(index int, val uint16) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]uint16, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceUint16 uses the specified function to replace each uint16s
+// by iterating each item.  The data in the returned result will be a
+// []uint16 containing the replaced items.
+func (v *Value) ReplaceUint16(replacer func(int, uint16) uint16) *Value {
+	arr := v.MustUint16Slice()
+	replaced := make([]uint16, len(arr))
+	v.EachUint16(func(index int, val uint16) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectUint16 uses the specified collector function to collect a value
+// for each of the uint16s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectUint16(collector func(int, uint16) interface{}) *Value {
+	arr := v.MustUint16Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachUint16(func(index int, val uint16) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Uint32 (uint32 and []uint32)
+*/
+
+// Uint32 gets the value as a uint32, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Uint32(optionalDefault ...uint32) uint32 {
+	if s, ok := v.data.(uint32); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustUint32 gets the value as a uint32.
+//
+// Panics if the object is not a uint32.
+func (v *Value) MustUint32() uint32 {
+	return v.data.(uint32)
+}
+
+// Uint32Slice gets the value as a []uint32, returns the optionalDefault
+// value or nil if the value is not a []uint32.
+func (v *Value) Uint32Slice(optionalDefault ...[]uint32) []uint32 {
+	if s, ok := v.data.([]uint32); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustUint32Slice gets the value as a []uint32.
+//
+// Panics if the object is not a []uint32.
+func (v *Value) MustUint32Slice() []uint32 {
+	return v.data.([]uint32)
+}
+
+// IsUint32 gets whether the object contained is a uint32 or not.
+func (v *Value) IsUint32() bool {
+	_, ok := v.data.(uint32)
+	return ok
+}
+
+// IsUint32Slice gets whether the object contained is a []uint32 or not.
+func (v *Value) IsUint32Slice() bool {
+	_, ok := v.data.([]uint32)
+	return ok
+}
+
+// EachUint32 calls the specified callback for each object
+// in the []uint32.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachUint32(callback func(int, uint32) bool) *Value {
+	for index, val := range v.MustUint32Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereUint32 uses the specified decider function to select items
+// from the []uint32.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereUint32(decider func(int, uint32) bool) *Value {
+	var selected []uint32
+	v.EachUint32(func(index int, val uint32) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupUint32 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]uint32.
+func (v *Value) GroupUint32(grouper func(int, uint32) string) *Value {
+	groups := make(map[string][]uint32)
+	v.EachUint32(func(index int, val uint32) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]uint32, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceUint32 uses the specified function to replace each uint32s
+// by iterating each item.  The data in the returned result will be a
+// []uint32 containing the replaced items.
+func (v *Value) ReplaceUint32(replacer func(int, uint32) uint32) *Value {
+	arr := v.MustUint32Slice()
+	replaced := make([]uint32, len(arr))
+	v.EachUint32(func(index int, val uint32) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectUint32 uses the specified collector function to collect a value
+// for each of the uint32s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectUint32(collector func(int, uint32) interface{}) *Value {
+	arr := v.MustUint32Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachUint32(func(index int, val uint32) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Uint64 (uint64 and []uint64)
+*/
+
+// Uint64 gets the value as a uint64, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Uint64(optionalDefault ...uint64) uint64 {
+	if s, ok := v.data.(uint64); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustUint64 gets the value as a uint64.
+//
+// Panics if the object is not a uint64.
+func (v *Value) MustUint64() uint64 {
+	return v.data.(uint64)
+}
+
+// Uint64Slice gets the value as a []uint64, returns the optionalDefault
+// value or nil if the value is not a []uint64.
+func (v *Value) Uint64Slice(optionalDefault ...[]uint64) []uint64 {
+	if s, ok := v.data.([]uint64); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustUint64Slice gets the value as a []uint64.
+//
+// Panics if the object is not a []uint64.
+func (v *Value) MustUint64Slice() []uint64 {
+	return v.data.([]uint64)
+}
+
+// IsUint64 gets whether the object contained is a uint64 or not.
+func (v *Value) IsUint64() bool {
+	_, ok := v.data.(uint64)
+	return ok
+}
+
+// IsUint64Slice gets whether the object contained is a []uint64 or not.
+func (v *Value) IsUint64Slice() bool {
+	_, ok := v.data.([]uint64)
+	return ok
+}
+
+// EachUint64 calls the specified callback for each object
+// in the []uint64.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachUint64(callback func(int, uint64) bool) *Value {
+	for index, val := range v.MustUint64Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereUint64 uses the specified decider function to select items
+// from the []uint64.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereUint64(decider func(int, uint64) bool) *Value {
+	var selected []uint64
+	v.EachUint64(func(index int, val uint64) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupUint64 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]uint64.
+func (v *Value) GroupUint64(grouper func(int, uint64) string) *Value {
+	groups := make(map[string][]uint64)
+	v.EachUint64(func(index int, val uint64) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]uint64, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceUint64 uses the specified function to replace each uint64s
+// by iterating each item.  The data in the returned result will be a
+// []uint64 containing the replaced items.
+func (v *Value) ReplaceUint64(replacer func(int, uint64) uint64) *Value {
+	arr := v.MustUint64Slice()
+	replaced := make([]uint64, len(arr))
+	v.EachUint64(func(index int, val uint64) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectUint64 uses the specified collector function to collect a value
+// for each of the uint64s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectUint64(collector func(int, uint64) interface{}) *Value {
+	arr := v.MustUint64Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachUint64(func(index int, val uint64) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Uintptr (uintptr and []uintptr)
+*/
+
+// Uintptr gets the value as a uintptr, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Uintptr(optionalDefault ...uintptr) uintptr {
+	if s, ok := v.data.(uintptr); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustUintptr gets the value as a uintptr.
+//
+// Panics if the object is not a uintptr.
+func (v *Value) MustUintptr() uintptr {
+	return v.data.(uintptr)
+}
+
+// UintptrSlice gets the value as a []uintptr, returns the optionalDefault
+// value or nil if the value is not a []uintptr.
+func (v *Value) UintptrSlice(optionalDefault ...[]uintptr) []uintptr {
+	if s, ok := v.data.([]uintptr); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustUintptrSlice gets the value as a []uintptr.
+//
+// Panics if the object is not a []uintptr.
+func (v *Value) MustUintptrSlice() []uintptr {
+	return v.data.([]uintptr)
+}
+
+// IsUintptr gets whether the object contained is a uintptr or not.
+func (v *Value) IsUintptr() bool {
+	_, ok := v.data.(uintptr)
+	return ok
+}
+
+// IsUintptrSlice gets whether the object contained is a []uintptr or not.
+func (v *Value) IsUintptrSlice() bool {
+	_, ok := v.data.([]uintptr)
+	return ok
+}
+
+// EachUintptr calls the specified callback for each object
+// in the []uintptr.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachUintptr(callback func(int, uintptr) bool) *Value {
+	for index, val := range v.MustUintptrSlice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereUintptr uses the specified decider function to select items
+// from the []uintptr.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereUintptr(decider func(int, uintptr) bool) *Value {
+	var selected []uintptr
+	v.EachUintptr(func(index int, val uintptr) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupUintptr uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]uintptr.
+func (v *Value) GroupUintptr(grouper func(int, uintptr) string) *Value {
+	groups := make(map[string][]uintptr)
+	v.EachUintptr(func(index int, val uintptr) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]uintptr, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceUintptr uses the specified function to replace each uintptrs
+// by iterating each item.  The data in the returned result will be a
+// []uintptr containing the replaced items.
+func (v *Value) ReplaceUintptr(replacer func(int, uintptr) uintptr) *Value {
+	arr := v.MustUintptrSlice()
+	replaced := make([]uintptr, len(arr))
+	v.EachUintptr(func(index int, val uintptr) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectUintptr uses the specified collector function to collect a value
+// for each of the uintptrs in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectUintptr(collector func(int, uintptr) interface{}) *Value {
+	arr := v.MustUintptrSlice()
+	collected := make([]interface{}, len(arr))
+	v.EachUintptr(func(index int, val uintptr) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Float32 (float32 and []float32)
+*/
+
+// Float32 gets the value as a float32, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Float32(optionalDefault ...float32) float32 {
+	if s, ok := v.data.(float32); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustFloat32 gets the value as a float32.
+//
+// Panics if the object is not a float32.
+func (v *Value) MustFloat32() float32 {
+	return v.data.(float32)
+}
+
+// Float32Slice gets the value as a []float32, returns the optionalDefault
+// value or nil if the value is not a []float32.
+func (v *Value) Float32Slice(optionalDefault ...[]float32) []float32 {
+	if s, ok := v.data.([]float32); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustFloat32Slice gets the value as a []float32.
+//
+// Panics if the object is not a []float32.
+func (v *Value) MustFloat32Slice() []float32 {
+	return v.data.([]float32)
+}
+
+// IsFloat32 gets whether the object contained is a float32 or not.
+func (v *Value) IsFloat32() bool {
+	_, ok := v.data.(float32)
+	return ok
+}
+
+// IsFloat32Slice gets whether the object contained is a []float32 or not.
+func (v *Value) IsFloat32Slice() bool {
+	_, ok := v.data.([]float32)
+	return ok
+}
+
+// EachFloat32 calls the specified callback for each object
+// in the []float32.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachFloat32(callback func(int, float32) bool) *Value {
+	for index, val := range v.MustFloat32Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereFloat32 uses the specified decider function to select items
+// from the []float32.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereFloat32(decider func(int, float32) bool) *Value {
+	var selected []float32
+	v.EachFloat32(func(index int, val float32) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupFloat32 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]float32.
+func (v *Value) GroupFloat32(grouper func(int, float32) string) *Value {
+	groups := make(map[string][]float32)
+	v.EachFloat32(func(index int, val float32) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]float32, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceFloat32 uses the specified function to replace each float32s
+// by iterating each item.  The data in the returned result will be a
+// []float32 containing the replaced items.
+func (v *Value) ReplaceFloat32(replacer func(int, float32) float32) *Value {
+	arr := v.MustFloat32Slice()
+	replaced := make([]float32, len(arr))
+	v.EachFloat32(func(index int, val float32) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectFloat32 uses the specified collector function to collect a value
+// for each of the float32s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectFloat32(collector func(int, float32) interface{}) *Value {
+	arr := v.MustFloat32Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachFloat32(func(index int, val float32) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Float64 (float64 and []float64)
+*/
+
+// Float64 gets the value as a float64, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Float64(optionalDefault ...float64) float64 {
+	if s, ok := v.data.(float64); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustFloat64 gets the value as a float64.
+//
+// Panics if the object is not a float64.
+func (v *Value) MustFloat64() float64 {
+	return v.data.(float64)
+}
+
+// Float64Slice gets the value as a []float64, returns the optionalDefault
+// value or nil if the value is not a []float64.
+func (v *Value) Float64Slice(optionalDefault ...[]float64) []float64 {
+	if s, ok := v.data.([]float64); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustFloat64Slice gets the value as a []float64.
+//
+// Panics if the object is not a []float64.
+func (v *Value) MustFloat64Slice() []float64 {
+	return v.data.([]float64)
+}
+
+// IsFloat64 gets whether the object contained is a float64 or not.
+func (v *Value) IsFloat64() bool {
+	_, ok := v.data.(float64)
+	return ok
+}
+
+// IsFloat64Slice gets whether the object contained is a []float64 or not.
+func (v *Value) IsFloat64Slice() bool {
+	_, ok := v.data.([]float64)
+	return ok
+}
+
+// EachFloat64 calls the specified callback for each object
+// in the []float64.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachFloat64(callback func(int, float64) bool) *Value {
+	for index, val := range v.MustFloat64Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereFloat64 uses the specified decider function to select items
+// from the []float64.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereFloat64(decider func(int, float64) bool) *Value {
+	var selected []float64
+	v.EachFloat64(func(index int, val float64) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupFloat64 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]float64.
+func (v *Value) GroupFloat64(grouper func(int, float64) string) *Value {
+	groups := make(map[string][]float64)
+	v.EachFloat64(func(index int, val float64) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]float64, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceFloat64 uses the specified function to replace each float64s
+// by iterating each item.  The data in the returned result will be a
+// []float64 containing the replaced items.
+func (v *Value) ReplaceFloat64(replacer func(int, float64) float64) *Value {
+	arr := v.MustFloat64Slice()
+	replaced := make([]float64, len(arr))
+	v.EachFloat64(func(index int, val float64) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectFloat64 uses the specified collector function to collect a value
+// for each of the float64s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectFloat64(collector func(int, float64) interface{}) *Value {
+	arr := v.MustFloat64Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachFloat64(func(index int, val float64) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Complex64 (complex64 and []complex64)
+*/
+
+// Complex64 gets the value as a complex64, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Complex64(optionalDefault ...complex64) complex64 {
+	if s, ok := v.data.(complex64); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustComplex64 gets the value as a complex64.
+//
+// Panics if the object is not a complex64.
+func (v *Value) MustComplex64() complex64 {
+	return v.data.(complex64)
+}
+
+// Complex64Slice gets the value as a []complex64, returns the optionalDefault
+// value or nil if the value is not a []complex64.
+func (v *Value) Complex64Slice(optionalDefault ...[]complex64) []complex64 {
+	if s, ok := v.data.([]complex64); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustComplex64Slice gets the value as a []complex64.
+//
+// Panics if the object is not a []complex64.
+func (v *Value) MustComplex64Slice() []complex64 {
+	return v.data.([]complex64)
+}
+
+// IsComplex64 gets whether the object contained is a complex64 or not.
+func (v *Value) IsComplex64() bool {
+	_, ok := v.data.(complex64)
+	return ok
+}
+
+// IsComplex64Slice gets whether the object contained is a []complex64 or not.
+func (v *Value) IsComplex64Slice() bool {
+	_, ok := v.data.([]complex64)
+	return ok
+}
+
+// EachComplex64 calls the specified callback for each object
+// in the []complex64.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachComplex64(callback func(int, complex64) bool) *Value {
+	for index, val := range v.MustComplex64Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereComplex64 uses the specified decider function to select items
+// from the []complex64.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereComplex64(decider func(int, complex64) bool) *Value {
+	var selected []complex64
+	v.EachComplex64(func(index int, val complex64) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupComplex64 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]complex64.
+func (v *Value) GroupComplex64(grouper func(int, complex64) string) *Value {
+	groups := make(map[string][]complex64)
+	v.EachComplex64(func(index int, val complex64) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]complex64, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceComplex64 uses the specified function to replace each complex64s
+// by iterating each item.  The data in the returned result will be a
+// []complex64 containing the replaced items.
+func (v *Value) ReplaceComplex64(replacer func(int, complex64) complex64) *Value {
+	arr := v.MustComplex64Slice()
+	replaced := make([]complex64, len(arr))
+	v.EachComplex64(func(index int, val complex64) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectComplex64 uses the specified collector function to collect a value
+// for each of the complex64s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectComplex64(collector func(int, complex64) interface{}) *Value {
+	arr := v.MustComplex64Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachComplex64(func(index int, val complex64) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
+
+/*
+	Complex128 (complex128 and []complex128)
+*/
+
+// Complex128 gets the value as a complex128, returns the optionalDefault
+// value or a system default object if the value is the wrong type.
+func (v *Value) Complex128(optionalDefault ...complex128) complex128 {
+	if s, ok := v.data.(complex128); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return 0
+}
+
+// MustComplex128 gets the value as a complex128.
+//
+// Panics if the object is not a complex128.
+func (v *Value) MustComplex128() complex128 {
+	return v.data.(complex128)
+}
+
+// Complex128Slice gets the value as a []complex128, returns the optionalDefault
+// value or nil if the value is not a []complex128.
+func (v *Value) Complex128Slice(optionalDefault ...[]complex128) []complex128 {
+	if s, ok := v.data.([]complex128); ok {
+		return s
+	}
+	if len(optionalDefault) == 1 {
+		return optionalDefault[0]
+	}
+	return nil
+}
+
+// MustComplex128Slice gets the value as a []complex128.
+//
+// Panics if the object is not a []complex128.
+func (v *Value) MustComplex128Slice() []complex128 {
+	return v.data.([]complex128)
+}
+
+// IsComplex128 gets whether the object contained is a complex128 or not.
+func (v *Value) IsComplex128() bool {
+	_, ok := v.data.(complex128)
+	return ok
+}
+
+// IsComplex128Slice gets whether the object contained is a []complex128 or not.
+func (v *Value) IsComplex128Slice() bool {
+	_, ok := v.data.([]complex128)
+	return ok
+}
+
+// EachComplex128 calls the specified callback for each object
+// in the []complex128.
+//
+// Panics if the object is the wrong type.
+func (v *Value) EachComplex128(callback func(int, complex128) bool) *Value {
+	for index, val := range v.MustComplex128Slice() {
+		carryon := callback(index, val)
+		if !carryon {
+			break
+		}
+	}
+	return v
+}
+
+// WhereComplex128 uses the specified decider function to select items
+// from the []complex128.  The object contained in the result will contain
+// only the selected items.
+func (v *Value) WhereComplex128(decider func(int, complex128) bool) *Value {
+	var selected []complex128
+	v.EachComplex128(func(index int, val complex128) bool {
+		shouldSelect := decider(index, val)
+		if !shouldSelect {
+			selected = append(selected, val)
+		}
+		return true
+	})
+	return &Value{data: selected}
+}
+
+// GroupComplex128 uses the specified grouper function to group the items
+// keyed by the return of the grouper.  The object contained in the
+// result will contain a map[string][]complex128.
+func (v *Value) GroupComplex128(grouper func(int, complex128) string) *Value {
+	groups := make(map[string][]complex128)
+	v.EachComplex128(func(index int, val complex128) bool {
+		group := grouper(index, val)
+		if _, ok := groups[group]; !ok {
+			groups[group] = make([]complex128, 0)
+		}
+		groups[group] = append(groups[group], val)
+		return true
+	})
+	return &Value{data: groups}
+}
+
+// ReplaceComplex128 uses the specified function to replace each complex128s
+// by iterating each item.  The data in the returned result will be a
+// []complex128 containing the replaced items.
+func (v *Value) ReplaceComplex128(replacer func(int, complex128) complex128) *Value {
+	arr := v.MustComplex128Slice()
+	replaced := make([]complex128, len(arr))
+	v.EachComplex128(func(index int, val complex128) bool {
+		replaced[index] = replacer(index, val)
+		return true
+	})
+	return &Value{data: replaced}
+}
+
+// CollectComplex128 uses the specified collector function to collect a value
+// for each of the complex128s in the slice.  The data returned will be a
+// []interface{}.
+func (v *Value) CollectComplex128(collector func(int, complex128) interface{}) *Value {
+	arr := v.MustComplex128Slice()
+	collected := make([]interface{}, len(arr))
+	v.EachComplex128(func(index int, val complex128) bool {
+		collected[index] = collector(index, val)
+		return true
+	})
+	return &Value{data: collected}
+}
diff --git a/vendor/github.com/stretchr/objx/value.go b/vendor/github.com/stretchr/objx/value.go
new file mode 100644
index 0000000000000000000000000000000000000000..956a2211d4d50954ceaef5f3de6a67668d2b8226
--- /dev/null
+++ b/vendor/github.com/stretchr/objx/value.go
@@ -0,0 +1,56 @@
+package objx
+
+import (
+	"fmt"
+	"strconv"
+)
+
+// Value provides methods for extracting interface{} data in various
+// types.
+type Value struct {
+	// data contains the raw data being managed by this Value
+	data interface{}
+}
+
+// Data returns the raw data contained by this Value
+func (v *Value) Data() interface{} {
+	return v.data
+}
+
+// String returns the value always as a string
+func (v *Value) String() string {
+	switch {
+	case v.IsStr():
+		return v.Str()
+	case v.IsBool():
+		return strconv.FormatBool(v.Bool())
+	case v.IsFloat32():
+		return strconv.FormatFloat(float64(v.Float32()), 'f', -1, 32)
+	case v.IsFloat64():
+		return strconv.FormatFloat(v.Float64(), 'f', -1, 64)
+	case v.IsInt():
+		return strconv.FormatInt(int64(v.Int()), 10)
+	case v.IsInt():
+		return strconv.FormatInt(int64(v.Int()), 10)
+	case v.IsInt8():
+		return strconv.FormatInt(int64(v.Int8()), 10)
+	case v.IsInt16():
+		return strconv.FormatInt(int64(v.Int16()), 10)
+	case v.IsInt32():
+		return strconv.FormatInt(int64(v.Int32()), 10)
+	case v.IsInt64():
+		return strconv.FormatInt(v.Int64(), 10)
+	case v.IsUint():
+		return strconv.FormatUint(uint64(v.Uint()), 10)
+	case v.IsUint8():
+		return strconv.FormatUint(uint64(v.Uint8()), 10)
+	case v.IsUint16():
+		return strconv.FormatUint(uint64(v.Uint16()), 10)
+	case v.IsUint32():
+		return strconv.FormatUint(uint64(v.Uint32()), 10)
+	case v.IsUint64():
+		return strconv.FormatUint(v.Uint64(), 10)
+	}
+
+	return fmt.Sprintf("%#v", v.Data())
+}
diff --git a/vendor/github.com/stretchr/testify/mock/doc.go b/vendor/github.com/stretchr/testify/mock/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..7324128ef19c51d1cc74cd2cae8c886b2a71e7df
--- /dev/null
+++ b/vendor/github.com/stretchr/testify/mock/doc.go
@@ -0,0 +1,44 @@
+// Package mock provides a system by which it is possible to mock your objects
+// and verify calls are happening as expected.
+//
+// Example Usage
+//
+// The mock package provides an object, Mock, that tracks activity on another object.  It is usually
+// embedded into a test object as shown below:
+//
+//   type MyTestObject struct {
+//     // add a Mock object instance
+//     mock.Mock
+//
+//     // other fields go here as normal
+//   }
+//
+// When implementing the methods of an interface, you wire your functions up
+// to call the Mock.Called(args...) method, and return the appropriate values.
+//
+// For example, to mock a method that saves the name and age of a person and returns
+// the year of their birth or an error, you might write this:
+//
+//     func (o *MyTestObject) SavePersonDetails(firstname, lastname string, age int) (int, error) {
+//       args := o.Called(firstname, lastname, age)
+//       return args.Int(0), args.Error(1)
+//     }
+//
+// The Int, Error and Bool methods are examples of strongly typed getters that take the argument
+// index position. Given this argument list:
+//
+//     (12, true, "Something")
+//
+// You could read them out strongly typed like this:
+//
+//     args.Int(0)
+//     args.Bool(1)
+//     args.String(2)
+//
+// For objects of your own type, use the generic Arguments.Get(index) method and make a type assertion:
+//
+//     return args.Get(0).(*MyObject), args.Get(1).(*AnotherObjectOfMine)
+//
+// This may cause a panic if the object you are getting is nil (the type assertion will fail), in those
+// cases you should check for nil first.
+package mock
diff --git a/vendor/github.com/stretchr/testify/mock/mock.go b/vendor/github.com/stretchr/testify/mock/mock.go
new file mode 100644
index 0000000000000000000000000000000000000000..fc63571d4e8b3a4d49fda3270f6af0de6c473b70
--- /dev/null
+++ b/vendor/github.com/stretchr/testify/mock/mock.go
@@ -0,0 +1,776 @@
+package mock
+
+import (
+	"fmt"
+	"reflect"
+	"regexp"
+	"runtime"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/davecgh/go-spew/spew"
+	"github.com/pmezard/go-difflib/difflib"
+	"github.com/stretchr/objx"
+	"github.com/stretchr/testify/assert"
+)
+
+// TestingT is an interface wrapper around *testing.T
+type TestingT interface {
+	Logf(format string, args ...interface{})
+	Errorf(format string, args ...interface{})
+	FailNow()
+}
+
+/*
+	Call
+*/
+
+// Call represents a method call and is used for setting expectations,
+// as well as recording activity.
+type Call struct {
+	Parent *Mock
+
+	// The name of the method that was or will be called.
+	Method string
+
+	// Holds the arguments of the method.
+	Arguments Arguments
+
+	// Holds the arguments that should be returned when
+	// this method is called.
+	ReturnArguments Arguments
+
+	// The number of times to return the return arguments when setting
+	// expectations. 0 means to always return the value.
+	Repeatability int
+
+	// Amount of times this call has been called
+	totalCalls int
+
+	// Holds a channel that will be used to block the Return until it either
+	// receives a message or is closed. nil means it returns immediately.
+	WaitFor <-chan time.Time
+
+	// Holds a handler used to manipulate arguments content that are passed by
+	// reference. It's useful when mocking methods such as unmarshalers or
+	// decoders.
+	RunFn func(Arguments)
+}
+
+func newCall(parent *Mock, methodName string, methodArguments ...interface{}) *Call {
+	return &Call{
+		Parent:          parent,
+		Method:          methodName,
+		Arguments:       methodArguments,
+		ReturnArguments: make([]interface{}, 0),
+		Repeatability:   0,
+		WaitFor:         nil,
+		RunFn:           nil,
+	}
+}
+
+func (c *Call) lock() {
+	c.Parent.mutex.Lock()
+}
+
+func (c *Call) unlock() {
+	c.Parent.mutex.Unlock()
+}
+
+// Return specifies the return arguments for the expectation.
+//
+//    Mock.On("DoSomething").Return(errors.New("failed"))
+func (c *Call) Return(returnArguments ...interface{}) *Call {
+	c.lock()
+	defer c.unlock()
+
+	c.ReturnArguments = returnArguments
+
+	return c
+}
+
+// Once indicates that that the mock should only return the value once.
+//
+//    Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Once()
+func (c *Call) Once() *Call {
+	return c.Times(1)
+}
+
+// Twice indicates that that the mock should only return the value twice.
+//
+//    Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Twice()
+func (c *Call) Twice() *Call {
+	return c.Times(2)
+}
+
+// Times indicates that that the mock should only return the indicated number
+// of times.
+//
+//    Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Times(5)
+func (c *Call) Times(i int) *Call {
+	c.lock()
+	defer c.unlock()
+	c.Repeatability = i
+	return c
+}
+
+// WaitUntil sets the channel that will block the mock's return until its closed
+// or a message is received.
+//
+//    Mock.On("MyMethod", arg1, arg2).WaitUntil(time.After(time.Second))
+func (c *Call) WaitUntil(w <-chan time.Time) *Call {
+	c.lock()
+	defer c.unlock()
+	c.WaitFor = w
+	return c
+}
+
+// After sets how long to block until the call returns
+//
+//    Mock.On("MyMethod", arg1, arg2).After(time.Second)
+func (c *Call) After(d time.Duration) *Call {
+	return c.WaitUntil(time.After(d))
+}
+
+// Run sets a handler to be called before returning. It can be used when
+// mocking a method such as unmarshalers that takes a pointer to a struct and
+// sets properties in such struct
+//
+//    Mock.On("Unmarshal", AnythingOfType("*map[string]interface{}").Return().Run(func(args Arguments) {
+//    	arg := args.Get(0).(*map[string]interface{})
+//    	arg["foo"] = "bar"
+//    })
+func (c *Call) Run(fn func(args Arguments)) *Call {
+	c.lock()
+	defer c.unlock()
+	c.RunFn = fn
+	return c
+}
+
+// On chains a new expectation description onto the mocked interface. This
+// allows syntax like.
+//
+//    Mock.
+//       On("MyMethod", 1).Return(nil).
+//       On("MyOtherMethod", 'a', 'b', 'c').Return(errors.New("Some Error"))
+func (c *Call) On(methodName string, arguments ...interface{}) *Call {
+	return c.Parent.On(methodName, arguments...)
+}
+
+// Mock is the workhorse used to track activity on another object.
+// For an example of its usage, refer to the "Example Usage" section at the top
+// of this document.
+type Mock struct {
+	// Represents the calls that are expected of
+	// an object.
+	ExpectedCalls []*Call
+
+	// Holds the calls that were made to this mocked object.
+	Calls []Call
+
+	// TestData holds any data that might be useful for testing.  Testify ignores
+	// this data completely allowing you to do whatever you like with it.
+	testData objx.Map
+
+	mutex sync.Mutex
+}
+
+// TestData holds any data that might be useful for testing.  Testify ignores
+// this data completely allowing you to do whatever you like with it.
+func (m *Mock) TestData() objx.Map {
+
+	if m.testData == nil {
+		m.testData = make(objx.Map)
+	}
+
+	return m.testData
+}
+
+/*
+	Setting expectations
+*/
+
+// On starts a description of an expectation of the specified method
+// being called.
+//
+//     Mock.On("MyMethod", arg1, arg2)
+func (m *Mock) On(methodName string, arguments ...interface{}) *Call {
+	for _, arg := range arguments {
+		if v := reflect.ValueOf(arg); v.Kind() == reflect.Func {
+			panic(fmt.Sprintf("cannot use Func in expectations. Use mock.AnythingOfType(\"%T\")", arg))
+		}
+	}
+
+	m.mutex.Lock()
+	defer m.mutex.Unlock()
+	c := newCall(m, methodName, arguments...)
+	m.ExpectedCalls = append(m.ExpectedCalls, c)
+	return c
+}
+
+// /*
+// 	Recording and responding to activity
+// */
+
+func (m *Mock) findExpectedCall(method string, arguments ...interface{}) (int, *Call) {
+	for i, call := range m.ExpectedCalls {
+		if call.Method == method && call.Repeatability > -1 {
+
+			_, diffCount := call.Arguments.Diff(arguments)
+			if diffCount == 0 {
+				return i, call
+			}
+
+		}
+	}
+	return -1, nil
+}
+
+func (m *Mock) findClosestCall(method string, arguments ...interface{}) (bool, *Call) {
+	diffCount := 0
+	var closestCall *Call
+
+	for _, call := range m.expectedCalls() {
+		if call.Method == method {
+
+			_, tempDiffCount := call.Arguments.Diff(arguments)
+			if tempDiffCount < diffCount || diffCount == 0 {
+				diffCount = tempDiffCount
+				closestCall = call
+			}
+
+		}
+	}
+
+	if closestCall == nil {
+		return false, nil
+	}
+
+	return true, closestCall
+}
+
+func callString(method string, arguments Arguments, includeArgumentValues bool) string {
+
+	var argValsString string
+	if includeArgumentValues {
+		var argVals []string
+		for argIndex, arg := range arguments {
+			argVals = append(argVals, fmt.Sprintf("%d: %#v", argIndex, arg))
+		}
+		argValsString = fmt.Sprintf("\n\t\t%s", strings.Join(argVals, "\n\t\t"))
+	}
+
+	return fmt.Sprintf("%s(%s)%s", method, arguments.String(), argValsString)
+}
+
+// Called tells the mock object that a method has been called, and gets an array
+// of arguments to return.  Panics if the call is unexpected (i.e. not preceded by
+// appropriate .On .Return() calls)
+// If Call.WaitFor is set, blocks until the channel is closed or receives a message.
+func (m *Mock) Called(arguments ...interface{}) Arguments {
+	// get the calling function's name
+	pc, _, _, ok := runtime.Caller(1)
+	if !ok {
+		panic("Couldn't get the caller information")
+	}
+	functionPath := runtime.FuncForPC(pc).Name()
+	//Next four lines are required to use GCCGO function naming conventions.
+	//For Ex:  github_com_docker_libkv_store_mock.WatchTree.pN39_github_com_docker_libkv_store_mock.Mock
+	//uses interface information unlike golang github.com/docker/libkv/store/mock.(*Mock).WatchTree
+	//With GCCGO we need to remove interface information starting from pN<dd>.
+	re := regexp.MustCompile("\\.pN\\d+_")
+	if re.MatchString(functionPath) {
+		functionPath = re.Split(functionPath, -1)[0]
+	}
+	parts := strings.Split(functionPath, ".")
+	functionName := parts[len(parts)-1]
+	return m.MethodCalled(functionName, arguments...)
+}
+
+// MethodCalled tells the mock object that the given method has been called, and gets
+// an array of arguments to return. Panics if the call is unexpected (i.e. not preceded
+// by appropriate .On .Return() calls)
+// If Call.WaitFor is set, blocks until the channel is closed or receives a message.
+func (m *Mock) MethodCalled(methodName string, arguments ...interface{}) Arguments {
+	m.mutex.Lock()
+	found, call := m.findExpectedCall(methodName, arguments...)
+
+	if found < 0 {
+		// we have to fail here - because we don't know what to do
+		// as the return arguments.  This is because:
+		//
+		//   a) this is a totally unexpected call to this method,
+		//   b) the arguments are not what was expected, or
+		//   c) the developer has forgotten to add an accompanying On...Return pair.
+
+		closestFound, closestCall := m.findClosestCall(methodName, arguments...)
+		m.mutex.Unlock()
+
+		if closestFound {
+			panic(fmt.Sprintf("\n\nmock: Unexpected Method Call\n-----------------------------\n\n%s\n\nThe closest call I have is: \n\n%s\n\n%s\n", callString(methodName, arguments, true), callString(methodName, closestCall.Arguments, true), diffArguments(arguments, closestCall.Arguments)))
+		} else {
+			panic(fmt.Sprintf("\nassert: mock: I don't know what to return because the method call was unexpected.\n\tEither do Mock.On(\"%s\").Return(...) first, or remove the %s() call.\n\tThis method was unexpected:\n\t\t%s\n\tat: %s", methodName, methodName, callString(methodName, arguments, true), assert.CallerInfo()))
+		}
+	}
+
+	switch {
+	case call.Repeatability == 1:
+		call.Repeatability = -1
+		call.totalCalls++
+
+	case call.Repeatability > 1:
+		call.Repeatability--
+		call.totalCalls++
+
+	case call.Repeatability == 0:
+		call.totalCalls++
+	}
+
+	// add the call
+	m.Calls = append(m.Calls, *newCall(m, methodName, arguments...))
+	m.mutex.Unlock()
+
+	// block if specified
+	if call.WaitFor != nil {
+		<-call.WaitFor
+	}
+
+	if call.RunFn != nil {
+		call.RunFn(arguments)
+	}
+
+	return call.ReturnArguments
+}
+
+/*
+	Assertions
+*/
+
+type assertExpectationser interface {
+	AssertExpectations(TestingT) bool
+}
+
+// AssertExpectationsForObjects asserts that everything specified with On and Return
+// of the specified objects was in fact called as expected.
+//
+// Calls may have occurred in any order.
+func AssertExpectationsForObjects(t TestingT, testObjects ...interface{}) bool {
+	for _, obj := range testObjects {
+		if m, ok := obj.(Mock); ok {
+			t.Logf("Deprecated mock.AssertExpectationsForObjects(myMock.Mock) use mock.AssertExpectationsForObjects(myMock)")
+			obj = &m
+		}
+		m := obj.(assertExpectationser)
+		if !m.AssertExpectations(t) {
+			return false
+		}
+	}
+	return true
+}
+
+// AssertExpectations asserts that everything specified with On and Return was
+// in fact called as expected.  Calls may have occurred in any order.
+func (m *Mock) AssertExpectations(t TestingT) bool {
+	m.mutex.Lock()
+	defer m.mutex.Unlock()
+	var somethingMissing bool
+	var failedExpectations int
+
+	// iterate through each expectation
+	expectedCalls := m.expectedCalls()
+	for _, expectedCall := range expectedCalls {
+		if !m.methodWasCalled(expectedCall.Method, expectedCall.Arguments) && expectedCall.totalCalls == 0 {
+			somethingMissing = true
+			failedExpectations++
+			t.Logf("\u274C\t%s(%s)", expectedCall.Method, expectedCall.Arguments.String())
+		} else {
+			if expectedCall.Repeatability > 0 {
+				somethingMissing = true
+				failedExpectations++
+			} else {
+				t.Logf("\u2705\t%s(%s)", expectedCall.Method, expectedCall.Arguments.String())
+			}
+		}
+	}
+
+	if somethingMissing {
+		t.Errorf("FAIL: %d out of %d expectation(s) were met.\n\tThe code you are testing needs to make %d more call(s).\n\tat: %s", len(expectedCalls)-failedExpectations, len(expectedCalls), failedExpectations, assert.CallerInfo())
+	}
+
+	return !somethingMissing
+}
+
+// AssertNumberOfCalls asserts that the method was called expectedCalls times.
+func (m *Mock) AssertNumberOfCalls(t TestingT, methodName string, expectedCalls int) bool {
+	m.mutex.Lock()
+	defer m.mutex.Unlock()
+	var actualCalls int
+	for _, call := range m.calls() {
+		if call.Method == methodName {
+			actualCalls++
+		}
+	}
+	return assert.Equal(t, expectedCalls, actualCalls, fmt.Sprintf("Expected number of calls (%d) does not match the actual number of calls (%d).", expectedCalls, actualCalls))
+}
+
+// AssertCalled asserts that the method was called.
+// It can produce a false result when an argument is a pointer type and the underlying value changed after calling the mocked method.
+func (m *Mock) AssertCalled(t TestingT, methodName string, arguments ...interface{}) bool {
+	m.mutex.Lock()
+	defer m.mutex.Unlock()
+	if !assert.True(t, m.methodWasCalled(methodName, arguments), fmt.Sprintf("The \"%s\" method should have been called with %d argument(s), but was not.", methodName, len(arguments))) {
+		t.Logf("%v", m.expectedCalls())
+		return false
+	}
+	return true
+}
+
+// AssertNotCalled asserts that the method was not called.
+// It can produce a false result when an argument is a pointer type and the underlying value changed after calling the mocked method.
+func (m *Mock) AssertNotCalled(t TestingT, methodName string, arguments ...interface{}) bool {
+	m.mutex.Lock()
+	defer m.mutex.Unlock()
+	if !assert.False(t, m.methodWasCalled(methodName, arguments), fmt.Sprintf("The \"%s\" method was called with %d argument(s), but should NOT have been.", methodName, len(arguments))) {
+		t.Logf("%v", m.expectedCalls())
+		return false
+	}
+	return true
+}
+
+func (m *Mock) methodWasCalled(methodName string, expected []interface{}) bool {
+	for _, call := range m.calls() {
+		if call.Method == methodName {
+
+			_, differences := Arguments(expected).Diff(call.Arguments)
+
+			if differences == 0 {
+				// found the expected call
+				return true
+			}
+
+		}
+	}
+	// we didn't find the expected call
+	return false
+}
+
+func (m *Mock) expectedCalls() []*Call {
+	return append([]*Call{}, m.ExpectedCalls...)
+}
+
+func (m *Mock) calls() []Call {
+	return append([]Call{}, m.Calls...)
+}
+
+/*
+	Arguments
+*/
+
+// Arguments holds an array of method arguments or return values.
+type Arguments []interface{}
+
+const (
+	// Anything is used in Diff and Assert when the argument being tested
+	// shouldn't be taken into consideration.
+	Anything string = "mock.Anything"
+)
+
+// AnythingOfTypeArgument is a string that contains the type of an argument
+// for use when type checking.  Used in Diff and Assert.
+type AnythingOfTypeArgument string
+
+// AnythingOfType returns an AnythingOfTypeArgument object containing the
+// name of the type to check for.  Used in Diff and Assert.
+//
+// For example:
+//	Assert(t, AnythingOfType("string"), AnythingOfType("int"))
+func AnythingOfType(t string) AnythingOfTypeArgument {
+	return AnythingOfTypeArgument(t)
+}
+
+// argumentMatcher performs custom argument matching, returning whether or
+// not the argument is matched by the expectation fixture function.
+type argumentMatcher struct {
+	// fn is a function which accepts one argument, and returns a bool.
+	fn reflect.Value
+}
+
+func (f argumentMatcher) Matches(argument interface{}) bool {
+	expectType := f.fn.Type().In(0)
+
+	if reflect.TypeOf(argument).AssignableTo(expectType) {
+		result := f.fn.Call([]reflect.Value{reflect.ValueOf(argument)})
+		return result[0].Bool()
+	}
+	return false
+}
+
+func (f argumentMatcher) String() string {
+	return fmt.Sprintf("func(%s) bool", f.fn.Type().In(0).Name())
+}
+
+// MatchedBy can be used to match a mock call based on only certain properties
+// from a complex struct or some calculation. It takes a function that will be
+// evaluated with the called argument and will return true when there's a match
+// and false otherwise.
+//
+// Example:
+// m.On("Do", MatchedBy(func(req *http.Request) bool { return req.Host == "example.com" }))
+//
+// |fn|, must be a function accepting a single argument (of the expected type)
+// which returns a bool. If |fn| doesn't match the required signature,
+// MatchedBy() panics.
+func MatchedBy(fn interface{}) argumentMatcher {
+	fnType := reflect.TypeOf(fn)
+
+	if fnType.Kind() != reflect.Func {
+		panic(fmt.Sprintf("assert: arguments: %s is not a func", fn))
+	}
+	if fnType.NumIn() != 1 {
+		panic(fmt.Sprintf("assert: arguments: %s does not take exactly one argument", fn))
+	}
+	if fnType.NumOut() != 1 || fnType.Out(0).Kind() != reflect.Bool {
+		panic(fmt.Sprintf("assert: arguments: %s does not return a bool", fn))
+	}
+
+	return argumentMatcher{fn: reflect.ValueOf(fn)}
+}
+
+// Get Returns the argument at the specified index.
+func (args Arguments) Get(index int) interface{} {
+	if index+1 > len(args) {
+		panic(fmt.Sprintf("assert: arguments: Cannot call Get(%d) because there are %d argument(s).", index, len(args)))
+	}
+	return args[index]
+}
+
+// Is gets whether the objects match the arguments specified.
+func (args Arguments) Is(objects ...interface{}) bool {
+	for i, obj := range args {
+		if obj != objects[i] {
+			return false
+		}
+	}
+	return true
+}
+
+// Diff gets a string describing the differences between the arguments
+// and the specified objects.
+//
+// Returns the diff string and number of differences found.
+func (args Arguments) Diff(objects []interface{}) (string, int) {
+
+	var output = "\n"
+	var differences int
+
+	var maxArgCount = len(args)
+	if len(objects) > maxArgCount {
+		maxArgCount = len(objects)
+	}
+
+	for i := 0; i < maxArgCount; i++ {
+		var actual, expected interface{}
+
+		if len(objects) <= i {
+			actual = "(Missing)"
+		} else {
+			actual = objects[i]
+		}
+
+		if len(args) <= i {
+			expected = "(Missing)"
+		} else {
+			expected = args[i]
+		}
+
+		if matcher, ok := expected.(argumentMatcher); ok {
+			if matcher.Matches(actual) {
+				output = fmt.Sprintf("%s\t%d: \u2705  %s matched by %s\n", output, i, actual, matcher)
+			} else {
+				differences++
+				output = fmt.Sprintf("%s\t%d: \u2705  %s not matched by %s\n", output, i, actual, matcher)
+			}
+		} else if reflect.TypeOf(expected) == reflect.TypeOf((*AnythingOfTypeArgument)(nil)).Elem() {
+
+			// type checking
+			if reflect.TypeOf(actual).Name() != string(expected.(AnythingOfTypeArgument)) && reflect.TypeOf(actual).String() != string(expected.(AnythingOfTypeArgument)) {
+				// not match
+				differences++
+				output = fmt.Sprintf("%s\t%d: \u274C  type %s != type %s - %s\n", output, i, expected, reflect.TypeOf(actual).Name(), actual)
+			}
+
+		} else {
+
+			// normal checking
+
+			if assert.ObjectsAreEqual(expected, Anything) || assert.ObjectsAreEqual(actual, Anything) || assert.ObjectsAreEqual(actual, expected) {
+				// match
+				output = fmt.Sprintf("%s\t%d: \u2705  %s == %s\n", output, i, actual, expected)
+			} else {
+				// not match
+				differences++
+				output = fmt.Sprintf("%s\t%d: \u274C  %s != %s\n", output, i, actual, expected)
+			}
+		}
+
+	}
+
+	if differences == 0 {
+		return "No differences.", differences
+	}
+
+	return output, differences
+
+}
+
+// Assert compares the arguments with the specified objects and fails if
+// they do not exactly match.
+func (args Arguments) Assert(t TestingT, objects ...interface{}) bool {
+
+	// get the differences
+	diff, diffCount := args.Diff(objects)
+
+	if diffCount == 0 {
+		return true
+	}
+
+	// there are differences... report them...
+	t.Logf(diff)
+	t.Errorf("%sArguments do not match.", assert.CallerInfo())
+
+	return false
+
+}
+
+// String gets the argument at the specified index. Panics if there is no argument, or
+// if the argument is of the wrong type.
+//
+// If no index is provided, String() returns a complete string representation
+// of the arguments.
+func (args Arguments) String(indexOrNil ...int) string {
+
+	if len(indexOrNil) == 0 {
+		// normal String() method - return a string representation of the args
+		var argsStr []string
+		for _, arg := range args {
+			argsStr = append(argsStr, fmt.Sprintf("%s", reflect.TypeOf(arg)))
+		}
+		return strings.Join(argsStr, ",")
+	} else if len(indexOrNil) == 1 {
+		// Index has been specified - get the argument at that index
+		var index = indexOrNil[0]
+		var s string
+		var ok bool
+		if s, ok = args.Get(index).(string); !ok {
+			panic(fmt.Sprintf("assert: arguments: String(%d) failed because object wasn't correct type: %s", index, args.Get(index)))
+		}
+		return s
+	}
+
+	panic(fmt.Sprintf("assert: arguments: Wrong number of arguments passed to String.  Must be 0 or 1, not %d", len(indexOrNil)))
+
+}
+
+// Int gets the argument at the specified index. Panics if there is no argument, or
+// if the argument is of the wrong type.
+func (args Arguments) Int(index int) int {
+	var s int
+	var ok bool
+	if s, ok = args.Get(index).(int); !ok {
+		panic(fmt.Sprintf("assert: arguments: Int(%d) failed because object wasn't correct type: %v", index, args.Get(index)))
+	}
+	return s
+}
+
+// Error gets the argument at the specified index. Panics if there is no argument, or
+// if the argument is of the wrong type.
+func (args Arguments) Error(index int) error {
+	obj := args.Get(index)
+	var s error
+	var ok bool
+	if obj == nil {
+		return nil
+	}
+	if s, ok = obj.(error); !ok {
+		panic(fmt.Sprintf("assert: arguments: Error(%d) failed because object wasn't correct type: %v", index, args.Get(index)))
+	}
+	return s
+}
+
+// Bool gets the argument at the specified index. Panics if there is no argument, or
+// if the argument is of the wrong type.
+func (args Arguments) Bool(index int) bool {
+	var s bool
+	var ok bool
+	if s, ok = args.Get(index).(bool); !ok {
+		panic(fmt.Sprintf("assert: arguments: Bool(%d) failed because object wasn't correct type: %v", index, args.Get(index)))
+	}
+	return s
+}
+
+func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) {
+	t := reflect.TypeOf(v)
+	k := t.Kind()
+
+	if k == reflect.Ptr {
+		t = t.Elem()
+		k = t.Kind()
+	}
+	return t, k
+}
+
+func diffArguments(expected Arguments, actual Arguments) string {
+	if len(expected) != len(actual) {
+		return fmt.Sprintf("Provided %v arguments, mocked for %v arguments", len(expected), len(actual))
+	}
+
+	for x := range expected {
+		if diffString := diff(expected[x], actual[x]); diffString != "" {
+			return fmt.Sprintf("Difference found in argument %v:\n\n%s", x, diffString)
+		}
+	}
+
+	return ""
+}
+
+// diff returns a diff of both values as long as both are of the same type and
+// are a struct, map, slice or array. Otherwise it returns an empty string.
+func diff(expected interface{}, actual interface{}) string {
+	if expected == nil || actual == nil {
+		return ""
+	}
+
+	et, ek := typeAndKind(expected)
+	at, _ := typeAndKind(actual)
+
+	if et != at {
+		return ""
+	}
+
+	if ek != reflect.Struct && ek != reflect.Map && ek != reflect.Slice && ek != reflect.Array {
+		return ""
+	}
+
+	e := spewConfig.Sdump(expected)
+	a := spewConfig.Sdump(actual)
+
+	diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
+		A:        difflib.SplitLines(e),
+		B:        difflib.SplitLines(a),
+		FromFile: "Expected",
+		FromDate: "",
+		ToFile:   "Actual",
+		ToDate:   "",
+		Context:  1,
+	})
+
+	return diff
+}
+
+var spewConfig = spew.ConfigState{
+	Indent:                  " ",
+	DisablePointerAddresses: true,
+	DisableCapacities:       true,
+	SortKeys:                true,
+}