Add ability to register HRSCommenters on Structs. (#3609)

Clients can register HRSCommenters to cause additional info
to be included as comments when generating the human readable
encoding for Noms Structs.
This commit is contained in:
Dan Willhite
2017-09-13 17:21:08 -07:00
committed by GitHub
parent 26eb9e3713
commit 10ec10dc00
9 changed files with 203 additions and 13 deletions

View File

@@ -168,6 +168,7 @@ See Spelling Values at https://github.com/attic-labs/noms/blob/master/doc/spelli
log.Flag("oneline", "show a summary of each commit on a single line").Bool()
log.Flag("graph", "show ascii-based commit hierarchy on left side of output").Bool()
log.Flag("show-value", "show commit value rather than diff information").Bool()
log.Flag("tz", "display formatted date comments in specified timezone, must be: local or utc").Enum("local", "utc")
log.Arg("path-spec", "").Required().String()
// merge
@@ -201,6 +202,7 @@ See Spelling Objects at https://github.com/attic-labs/noms/blob/master/doc/spell
`)
show.Flag("raw", "If true, dumps the raw binary version of the data").Bool()
show.Flag("stats", "If true, reports statistics related to the value").Bool()
show.Flag("tz", "display formatted date comments in specified timezone, must be: local or utc").Enum("local", "utc")
show.Arg("object", "a noms object").Required().String()
// sync

View File

@@ -164,7 +164,7 @@ func (s *nomsCommitTestSuite) TestNomsCommitMetadata() {
metaOld = metaNew
stdoutString, stderrString = s.MustRun(main, []string{"commit", "--allow-dupe=1", "--meta=message=bar", "--date=" + spec.CommitMetaDateFormat, dsName + ".value", sp.String()})
stdoutString, stderrString = s.MustRun(main, []string{"commit", "--allow-dupe=1", "--meta=message=bar", "--date=" + spec.CommitMetaDateFormat[:20], dsName + ".value", sp.String()})
s.Empty(stderrString)
s.Contains(stdoutString, "New head #")

View File

@@ -6,11 +6,13 @@ package main
import (
"bytes"
"errors"
"fmt"
"io"
"math"
"os"
"strings"
"time"
"github.com/attic-labs/noms/cmd/util"
"github.com/attic-labs/noms/go/config"
@@ -57,6 +59,7 @@ func setupLogFlags() *flag.FlagSet {
logFlagSet.BoolVar(&oneline, "oneline", false, "show a summary of each commit on a single line")
logFlagSet.BoolVar(&showGraph, "graph", false, "show ascii-based commit hierarchy on left side of output")
logFlagSet.BoolVar(&showValue, "show-value", false, "show commit value rather than diff information")
logFlagSet.StringVar(&tzName, "tz", "local", "display formatted date comments in specified timezone, must be: local or utc")
outputpager.RegisterOutputpagerFlags(logFlagSet)
verbose.RegisterVerboseFlags(logFlagSet)
return logFlagSet
@@ -66,6 +69,9 @@ func runLog(args []string) int {
useColor = shouldUseColor()
cfg := config.NewResolver()
tz, _ := locationFromTimezoneArg(tzName, nil)
datetime.RegisterHRSCommenter(tz)
resolved := cfg.ResolvePathSpec(args[0])
sp, err := spec.ForPath(resolved)
d.CheckErrorNoUsage(err)
@@ -107,7 +113,7 @@ func runLog(args []string) int {
go func(ch chan []byte, node LogNode) {
buff := &bytes.Buffer{}
printCommit(node, path, buff, database)
printCommit(node, path, buff, database, tz)
ch <- buff.Bytes()
}(ch, ln)
@@ -135,7 +141,7 @@ func runLog(args []string) int {
// Prints the information for one commit in the log, including ascii graph on left side of commits if
// -graph arg is true.
func printCommit(node LogNode, path types.Path, w io.Writer, db datas.Database) (err error) {
func printCommit(node LogNode, path types.Path, w io.Writer, db datas.Database, tz *time.Location) (err error) {
maxMetaFieldNameLength := func(commit types.Struct) int {
maxLen := 0
if m, ok := commit.MaybeGet(datas.MetaField); ok {
@@ -181,7 +187,7 @@ func printCommit(node LogNode, path types.Path, w io.Writer, db datas.Database)
lineno := 1
if maxLines != 0 {
lineno, err = writeMetaLines(node, maxLines, lineno, maxFieldNameLen, w)
lineno, err = writeMetaLines(node, maxLines, lineno, maxFieldNameLen, w, tz)
if err != nil && err != writers.MaxLinesErr {
fmt.Fprintf(w, "error: %s\n", err)
return
@@ -249,7 +255,7 @@ func genGraph(node LogNode, lineno int) string {
return string(buf)
}
func writeMetaLines(node LogNode, maxLines, lineno, maxLabelLen int, w io.Writer) (int, error) {
func writeMetaLines(node LogNode, maxLines, lineno, maxLabelLen int, w io.Writer, tz *time.Location) (int, error) {
if m, ok := node.commit.MaybeGet(datas.MetaField); ok {
genPrefix := func(w *writers.PrefixWriter) []byte {
return []byte(genGraph(node, int(w.NumLines)))
@@ -261,14 +267,16 @@ func writeMetaLines(node LogNode, maxLines, lineno, maxLabelLen int, w io.Writer
types.TypeOf(meta).Desc.(types.StructDesc).IterFields(func(fieldName string, t *types.Type, optional bool) {
v := meta.Get(fieldName)
fmt.Fprintf(pw, "%-*s", maxLabelLen+2, strings.Title(fieldName)+":")
// Encode dates as formatted string if this is a top-level meta
// field of type datetime.DateTimeType
if types.TypeOf(v).Equals(datetime.DateTimeType) {
var dt datetime.DateTime
dt.UnmarshalNoms(v)
fmt.Fprintf(pw, dt.Format(spec.CommitMetaDateFormat))
fmt.Fprintln(pw, dt.In(tz).Format(time.RFC3339))
} else {
types.WriteEncodedValue(pw, v)
}
fmt.Fprintf(pw, "\n")
fmt.Fprintln(pw)
})
})
return int(pw.NumLines), err
@@ -374,3 +382,16 @@ func min(i, j int) int {
}
return j
}
func locationFromTimezoneArg(tz string, defaultTZ *time.Location) (*time.Location, error) {
switch tz {
case "local":
return time.Local, nil
case "utc":
return time.UTC, nil
case "":
return defaultTZ, nil
default:
return nil, errors.New("value must be: local or utc")
}
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/attic-labs/noms/go/config"
"github.com/attic-labs/noms/go/d"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/go/util/datetime"
"github.com/attic-labs/noms/go/util/outputpager"
"github.com/attic-labs/noms/go/util/verbose"
flag "github.com/juju/gnuflag"
@@ -28,8 +29,11 @@ var nomsShow = &util.Command{
Nargs: 1,
}
var showRaw = false
var showStats = false
var (
showRaw = false
showStats = false
tzName string
)
func setupShowFlags() *flag.FlagSet {
showFlagSet := flag.NewFlagSet("show", flag.ExitOnError)
@@ -37,6 +41,7 @@ func setupShowFlags() *flag.FlagSet {
verbose.RegisterVerboseFlags(showFlagSet)
showFlagSet.BoolVar(&showRaw, "raw", false, "If true, dumps the raw binary version of the data")
showFlagSet.BoolVar(&showStats, "stats", false, "If true, reports statistics related to the value")
showFlagSet.StringVar(&tzName, "tz", "local", "display formatted date comments in specified timezone, must be: local or utc")
return showFlagSet
}
@@ -69,6 +74,9 @@ func runShow(args []string) int {
return 0
}
tz, _ := locationFromTimezoneArg(tzName, nil)
datetime.RegisterHRSCommenter(tz)
pgr := outputpager.Start()
defer pgr.Stop()

View File

@@ -15,7 +15,7 @@ import (
flag "github.com/juju/gnuflag"
)
const CommitMetaDateFormat = "2006-01-02T15:04:05-0700"
const CommitMetaDateFormat = time.RFC3339
var (
commitMetaDate string
@@ -101,7 +101,7 @@ func CreateCommitMetaStruct(db datas.Database, date, message string, keyValueStr
} else {
_, err := time.Parse(CommitMetaDateFormat, date)
if err != nil {
return types.EmptyStruct, errors.New(fmt.Sprintf("Unable to parse date: %s", date))
return types.EmptyStruct, errors.New(fmt.Sprintf("Unable to parse date: %s, error: %s", date, err))
}
}
metaValues["date"] = types.String(date)

View File

@@ -9,12 +9,80 @@ import (
"fmt"
"io"
"strconv"
"sync"
"github.com/attic-labs/noms/go/d"
"github.com/attic-labs/noms/go/util/writers"
humanize "github.com/dustin/go-humanize"
)
// Clients can register a 'commenter' to return a comment that will get appended
// to the first line of encoded values. For example, the noms DateTime struct
// normally gets encoded as follows:
// lastRefresh: DateTime {
// secSinceEpoch: 1.501801626877e+09,
// }
//
// By registering a commenter that returns a nicely formatted date,
// the struct will be coded with a comment:
// lastRefresh: DateTime { // 2017-08-03T16:07:06-07:00
// secSinceEpoch: 1.501801626877e+09,
// }
// Function type for commenter functions
type HRSCommenter interface {
Comment(Value) string
}
var (
commenterRegistry = map[string]map[string]HRSCommenter{}
registryLock sync.RWMutex
)
// RegisterHRSCommenter is called to with three arguments:
// typename: the name of the struct this function will be applied to
// unique: an arbitrary string to differentiate functions that should be applied
// to different structs that have the same name (e.g. two implementations of
// the "Employee" type.
// commenter: an interface with a 'Comment()' function that gets called for all
// Values with this name. The function should verify the type of the Value
// and, if appropriate, return a non-empty string to be appended as the comment
func RegisterHRSCommenter(typename, unique string, commenter HRSCommenter) {
registryLock.Lock()
defer registryLock.Unlock()
commenters := commenterRegistry[typename]
if commenters == nil {
commenters = map[string]HRSCommenter{}
commenterRegistry[typename] = commenters
}
commenters[unique] = commenter
}
// UnregisterHRSCommenter will remove a commenter function for a specified
// typename/unique string combination.
func UnregisterHRSCommenter(typename, unique string) {
registryLock.Lock()
defer registryLock.Unlock()
r := commenterRegistry[typename]
if r == nil {
return
}
delete(r, unique)
}
// GetHRSCommenters the map of 'unique' strings to HRSCommentFunc for
// a specified typename.
func GetHRSCommenters(typename string) []HRSCommenter {
registryLock.RLock()
defer registryLock.RUnlock()
// need to copy this value so we can release the lock
commenters := []HRSCommenter{}
for _, f := range commenterRegistry[typename] {
commenters = append(commenters, f)
}
return commenters
}
// Human Readable Serialization
type hrsWriter struct {
ind int
@@ -190,6 +258,14 @@ func (w *hrsWriter) writeStruct(v Struct) {
w.write(" ")
}
w.write("{")
commenters := GetHRSCommenters(v.name)
for _, commenter := range commenters {
if comment := commenter.Comment(v); comment != "" {
w.write(" // " + comment)
break
}
}
w.indent()
if len(v.fieldNames) > 0 {

View File

@@ -322,3 +322,37 @@ func TestWriteHumanReadableStructOptionalFields(t *testing.T) {
StructField{"b", BoolType, true})
assertWriteHRSEqual(t, "Struct S1 {\n a: Bool,\n b?: Bool,\n}", typ)
}
type TestCommenter struct {
prefix string
testType *Type
}
func (c TestCommenter) Comment(v Value) string {
if !(v.typeOf().Equals(c.testType)) {
return ""
}
return c.prefix + string(v.(Struct).Get("Name").(String))
}
func TestRegisterCommenter(t *testing.T) {
a := assert.New(t)
tt := NewStruct("TestType1", StructData{"Name": String("abc-123")})
nt := NewStruct("TestType2", StructData{"Name": String("abc-123")})
RegisterHRSCommenter("TestType1", "mylib1", TestCommenter{prefix: "MyTest: ", testType: tt.typeOf()})
s1 := EncodedValue(tt)
a.True(strings.Contains(s1, "// MyTest: abc-123"))
s1 = EncodedValue(nt)
a.False(strings.Contains(s1, "// MyTest: abc-123"))
RegisterHRSCommenter("TestType1", "mylib1", TestCommenter{prefix: "MyTest2: ", testType: tt.typeOf()})
s1 = EncodedValue(tt)
a.True(strings.Contains(s1, "// MyTest2: abc-123"))
UnregisterHRSCommenter("TestType1", "mylib1")
s1 = EncodedValue(tt)
a.False(strings.Contains(s1, "// MyTest2: abc-123"))
}

View File

@@ -14,6 +14,11 @@ import (
"github.com/attic-labs/noms/go/types"
)
const (
datetypename = "DateTime"
hrsEncodingName = "noms-datetime"
)
// DateTime implements marshaling of time.Time to and from Noms.
type DateTime struct {
time.Time
@@ -22,16 +27,20 @@ type DateTime struct {
// DateTimeType is the Noms type used to represent date time objects in Noms.
// The field secSinceEpoch may contain fractions in cases where seconds are
// not sufficient.
var DateTimeType = types.MakeStructTypeFromFields("DateTime", types.FieldMap{
var DateTimeType = types.MakeStructTypeFromFields(datetypename, types.FieldMap{
"secSinceEpoch": types.NumberType,
})
var dateTimeTemplate = types.MakeStructTemplate("DateTime", []string{"secSinceEpoch"})
var dateTimeTemplate = types.MakeStructTemplate(datetypename, []string{"secSinceEpoch"})
// Epoch is the unix Epoch. This time is very consistent,
// which makes it useful for testing or checking for uninitialized values
var Epoch = DateTime{time.Unix(0, 0)}
func init() {
RegisterHRSCommenter(time.Local)
}
// Now is an alias for a DateTime initialized with time.Now()
func Now() DateTime {
return DateTime{time.Now()}
@@ -65,3 +74,21 @@ func (dt *DateTime) UnmarshalNoms(v types.Value) error {
*dt = DateTime{time.Unix(int64(s), int64(frac*1e9))}
return nil
}
type DateTimeCommenter struct {
tz *time.Location
}
func (c DateTimeCommenter) Comment(v types.Value) string {
if !types.IsValueSubtypeOf(v, DateTimeType) {
return ""
}
var dt DateTime
marshal.MustUnmarshal(v, &dt)
return dt.In(c.tz).Format(time.RFC3339)
}
func RegisterHRSCommenter(tz *time.Location) {
hrsCommenter := DateTimeCommenter{tz: tz}
types.RegisterHRSCommenter(datetypename, hrsEncodingName, hrsCommenter)
}

View File

@@ -5,6 +5,7 @@
package datetime
import (
"strings"
"testing"
"time"
@@ -151,3 +152,24 @@ func TestEpoch(t *testing.T) {
assert := assert.New(t)
assert.Equal(Epoch, DateTime{time.Unix(0, 0)})
}
func TestHRSComment(t *testing.T) {
a := assert.New(t)
vs := newTestValueStore()
dt := Now()
mdt := marshal.MustMarshal(vs, dt)
exp := dt.Format(time.RFC3339)
s1 := types.EncodedValue(mdt)
a.True(strings.Contains(s1, "{ // "+exp))
RegisterHRSCommenter(time.UTC)
exp = dt.In(time.UTC).Format((time.RFC3339))
s1 = types.EncodedValue(mdt)
a.True(strings.Contains(s1, "{ // "+exp))
types.UnregisterHRSCommenter(datetypename, hrsEncodingName)
s1 = types.EncodedValue(mdt)
a.False(strings.Contains(s1, "{ // 20"))
}