From c9bd2f9168f3aa70155fc8ebef3156f6df42a552 Mon Sep 17 00:00:00 2001
From: Tamer Tas <contact@tmrts.com>
Date: Sat, 19 Dec 2015 12:47:24 +0200
Subject: [PATCH] Create metadata for templates

---
 pkg/cmd/download.go           |  8 ++++--
 pkg/cmd/metadata.go           | 30 +++++++++++++++++++++
 pkg/cmd/save.go               |  5 ++++
 pkg/template/metadata.go      | 51 +++++++++++++++++++++++++++++++++++
 pkg/template/metadata_test.go | 27 +++++++++++++++++++
 pkg/template/template.go      | 46 ++++++++++++++++++++++++++-----
 pkg/tmplt/configuration.go    | 17 ++++++++++--
 7 files changed, 174 insertions(+), 10 deletions(-)
 create mode 100644 pkg/cmd/metadata.go
 create mode 100644 pkg/template/metadata.go
 create mode 100644 pkg/template/metadata_test.go

diff --git a/pkg/cmd/download.go b/pkg/cmd/download.go
index 2e3d536..35df834 100644
--- a/pkg/cmd/download.go
+++ b/pkg/cmd/download.go
@@ -59,7 +59,7 @@ func downloadZip(URL, targetDir string) error {
 			defer rc.Close()
 		}
 
-		// split first token of f.Name since it's zip file name
+		// splits the first token of f.Name since it's zip file name
 		path := filepath.Join(dest, strings.SplitAfterN(f.Name, "/", 2)[1])
 
 		if f.FileInfo().IsDir() {
@@ -89,6 +89,7 @@ func downloadZip(URL, targetDir string) error {
 		}
 	}
 
+	// TODO Wrap this function in a validation wrapper from top to bottom
 	if _, err := util.ValidateTemplate(targetDir); err != nil {
 		return err
 	}
@@ -134,7 +135,6 @@ var Download = &cli.Command{
 
 		zipURL := host.ZipURL(templateURL)
 
-		// TODO validate template as well
 		if err := downloadZip(zipURL, targetDir); err != nil {
 			// Delete if download transaction fails
 			defer os.RemoveAll(targetDir)
@@ -142,6 +142,10 @@ var Download = &cli.Command{
 			exit.Error(fmt.Errorf("download: %s", err))
 		}
 
+		if err := serializeMetadata(templateName, templateURL, targetDir); err != nil {
+			exit.Error(fmt.Errorf("download: %s", err))
+		}
+
 		exit.OK("Successfully downloaded the template %v", templateName)
 	},
 }
diff --git a/pkg/cmd/metadata.go b/pkg/cmd/metadata.go
new file mode 100644
index 0000000..df06177
--- /dev/null
+++ b/pkg/cmd/metadata.go
@@ -0,0 +1,30 @@
+package cmd
+
+import (
+	"encoding/json"
+	"os"
+	"path/filepath"
+
+	"github.com/tmrts/tmplt/pkg/template"
+	"github.com/tmrts/tmplt/pkg/tmplt"
+)
+
+func serializeMetadata(tag string, repo string, targetDir string) error {
+	fname := filepath.Join(targetDir, tmplt.TemplateMetadataName)
+
+	f, err := os.Create(fname)
+	if err != nil {
+		return err
+	} else {
+		defer f.Close()
+	}
+
+	enc := json.NewEncoder(f)
+
+	t := template.Metadata{tag, repo, template.NewTime()}
+	if err := enc.Encode(&t); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/cmd/save.go b/pkg/cmd/save.go
index bcdeafe..4e70719 100644
--- a/pkg/cmd/save.go
+++ b/pkg/cmd/save.go
@@ -15,6 +15,7 @@ import (
 )
 
 var Save = &cli.Command{
+	// TODO rename template-name to template-tag
 	Use:   "save <template-path> <template-name>",
 	Short: "Save a project template to local template registry",
 	Run: func(c *cli.Command, args []string) {
@@ -53,6 +54,10 @@ var Save = &cli.Command{
 			exit.Error(err)
 		}
 
+		if err := serializeMetadata(templateName, "local:"+tmplDir, targetDir); err != nil {
+			exit.Error(fmt.Errorf("save: %s", err))
+		}
+
 		exit.OK("Successfully saved the template %v", templateName)
 	},
 }
diff --git a/pkg/template/metadata.go b/pkg/template/metadata.go
new file mode 100644
index 0000000..97ad507
--- /dev/null
+++ b/pkg/template/metadata.go
@@ -0,0 +1,51 @@
+package template
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/docker/go-units"
+)
+
+type Metadata struct {
+	Tag        string
+	Repository string
+
+	Created JSONTime
+}
+
+func (m Metadata) String() []string {
+	tDelta := time.Now().Sub(time.Time(m.Created))
+	return []string{m.Tag, m.Repository, units.HumanDuration(tDelta) + " ago"}
+}
+
+type JSONTime time.Time
+
+const (
+	timeFormat = "Mon Jan 2 15:04 -0700 MST 2006"
+)
+
+func NewTime() JSONTime {
+	return JSONTime(time.Now())
+}
+
+func (t *JSONTime) MarshalJSON() ([]byte, error) {
+	stamp := fmt.Sprintf(`"%s"`, time.Time(*t).Format(timeFormat))
+
+	return []byte(stamp), nil
+}
+
+func (t *JSONTime) UnmarshalJSON(b []byte) error {
+	time, err := time.Parse(timeFormat, string(b)[1:len(b)-1])
+	if err != nil {
+		return err
+	}
+
+	*t = JSONTime(time)
+
+	return nil
+}
+
+func (t JSONTime) String() string {
+	return fmt.Sprintf("%s", time.Time(t).Format(timeFormat))
+}
diff --git a/pkg/template/metadata_test.go b/pkg/template/metadata_test.go
new file mode 100644
index 0000000..6f2e0e1
--- /dev/null
+++ b/pkg/template/metadata_test.go
@@ -0,0 +1,27 @@
+package template_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/tmrts/tmplt/pkg/template"
+)
+
+func TestMarshalsTime(t *testing.T) {
+	jsonT := template.NewTime()
+
+	b, err := jsonT.MarshalJSON()
+	if err != nil {
+		t.Error(err)
+	}
+
+	var unmarshaledT template.JSONTime
+	if err := json.Unmarshal(b, &unmarshaledT); err != nil {
+		t.Error(err)
+	}
+
+	expected, got := jsonT.String(), unmarshaledT.String()
+	if expected != got {
+		t.Errorf("marshaled and unmarshaled time should've been equal expected %q, got %q", expected, got)
+	}
+}
diff --git a/pkg/template/template.go b/pkg/template/template.go
index f482e91..14494a3 100644
--- a/pkg/template/template.go
+++ b/pkg/template/template.go
@@ -9,12 +9,18 @@ import (
 
 	"github.com/tmrts/tmplt/pkg/prompt"
 	"github.com/tmrts/tmplt/pkg/tmplt"
+	"github.com/tmrts/tmplt/pkg/util/osutil"
 	"github.com/tmrts/tmplt/pkg/util/stringutil"
 )
 
 type Interface interface {
 	Execute(string) error
 	UseDefaultValues()
+	Info() Metadata
+}
+
+func (t dirTemplate) Info() Metadata {
+	return t.Metadata
 }
 
 func Get(path string) (Interface, error) {
@@ -49,18 +55,44 @@ func Get(path string) (Interface, error) {
 		return metadata, nil
 	}(filepath.Join(absPath, tmplt.ContextFileName))
 
+	metadataExists, err := osutil.FileExists(filepath.Join(absPath, tmplt.TemplateMetadataName))
+	if err != nil {
+		return nil, err
+	}
+
+	md, err := func() (Metadata, error) {
+		if !metadataExists {
+			return Metadata{}, nil
+		}
+
+		b, err := ioutil.ReadFile(filepath.Join(absPath, tmplt.TemplateMetadataName))
+		if err != nil {
+			return Metadata{}, err
+		}
+
+		var m Metadata
+		if err := json.Unmarshal(b, &m); err != nil {
+			return Metadata{}, err
+		}
+
+		return m, nil
+	}()
+
 	return &dirTemplate{
-		Context: ctxt,
-		FuncMap: FuncMap,
-		Path:    filepath.Join(absPath, tmplt.TemplateDirName),
+		Context:  ctxt,
+		FuncMap:  FuncMap,
+		Path:     filepath.Join(absPath, tmplt.TemplateDirName),
+		Metadata: md,
 	}, err
 }
 
 type dirTemplate struct {
-	Path    string
-	Context map[string]interface{}
-	FuncMap template.FuncMap
+	Path     string
+	Context  map[string]interface{}
+	FuncMap  template.FuncMap
+	Metadata Metadata
 
+	alignment         string
 	ShouldUseDefaults bool
 }
 
@@ -73,6 +105,7 @@ func (t *dirTemplate) BindPrompts() {
 		for s, v := range t.Context {
 			t.FuncMap[s] = func() interface{} {
 				switch v := v.(type) {
+				// First is the default value if it's a slice
 				case []interface{}:
 					return v[0]
 				}
@@ -93,6 +126,7 @@ func (t *dirTemplate) Execute(dirPrefix string) error {
 
 	// TODO create io.ReadWriter from string
 	// TODO refactor name manipulation
+	// TODO trim leading or trailing whitespaces
 	return filepath.Walk(t.Path, func(filename string, info os.FileInfo, err error) error {
 		if err != nil {
 			return err
diff --git a/pkg/tmplt/configuration.go b/pkg/tmplt/configuration.go
index bd8ef29..c96b19a 100644
--- a/pkg/tmplt/configuration.go
+++ b/pkg/tmplt/configuration.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/tmrts/tmplt/pkg/util/exit"
 	"github.com/tmrts/tmplt/pkg/util/osutil"
+	"github.com/tmrts/tmplt/pkg/util/tlog"
 )
 
 const (
@@ -19,8 +20,9 @@ const (
 	ConfigFileName = "config.json"
 	TemplateDir    = "templates"
 
-	ContextFileName = "project.json"
-	TemplateDirName = "template"
+	ContextFileName      = "project.json"
+	TemplateDirName      = "template"
+	TemplateMetadataName = "__metadata.json"
 
 	GithubOwner = "tmrts"
 	GithubRepo  = "tmplt"
@@ -45,6 +47,17 @@ func init() {
 	Configuration.FilePath = filepath.Join(homeDir, ConfigDirPath, ConfigFileName)
 	Configuration.TemplateDirPath = filepath.Join(homeDir, ConfigDirPath, TemplateDir)
 
+	IsTemplateDirInitialized, err := osutil.DirExists(Configuration.TemplateDirPath)
+	if err != nil {
+		exit.Error(err)
+	}
+
+	// TODO perform this in related commands only with ValidateInitialization
+	if !IsTemplateDirInitialized {
+		tlog.Warn("Template registry is not initialized. Please run `init` command to create it.")
+		return
+	}
+
 	// Read .config/tmplt/config.json if exists
 	// TODO use defaults if config.json doesn't exist
 	hasConfig, err := osutil.FileExists(Configuration.FilePath)
-- 
GitLab