Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create separate context.go file and add tests for ContextTime. #628

Merged
merged 3 commits into from
Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 53 additions & 0 deletions claat/types/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package types

import (
"bytes"
"time"
)

// Context is an export context.
// It is defined in this package so that it can be used by both cli and a server.
type Context struct {
Env string `json:"environment"` // Current export environment
Format string `json:"format"` // Output format, e.g. "html"
Prefix string `json:"prefix,omitempty"` // Assets URL prefix for HTML-based formats
MainGA string `json:"mainga,omitempty"` // Global Google Analytics ID
Updated *ContextTime `json:"updated,omitempty"` // Last update timestamp
}

// ContextMeta is a composition of export context and meta data.
type ContextMeta struct {
Context
Meta
}

// ContextTime is a wrapper around time.Time so we can implement JSON marshalling.
type ContextTime time.Time

// MarshalJSON implements Marshaler interface.
// The output format is RFC3339.
func (ct ContextTime) MarshalJSON() ([]byte, error) {
v := time.Time(ct).Format(time.RFC3339)
b := make([]byte, len(v)+2)
b[0] = '"'
b[len(b)-1] = '"'
copy(b[1:], v)
return b, nil
}

// UnmarshalJSON implements Unmarshaler interface.
// Accepted formats:
// - RFC3339
// - YYYY-MM-DD
func (ct *ContextTime) UnmarshalJSON(b []byte) error {
b = bytes.Trim(b, `"`)
t, err := time.Parse(time.RFC3339, string(b))
if err != nil {
t, err = time.Parse("2006-01-02", string(b))
}
if err != nil {
return err
}
*ct = ContextTime(t)
return nil
}
132 changes: 132 additions & 0 deletions claat/types/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package types

import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
)

func TestContextTimeMarshalJSON(t *testing.T) {
tests := []struct {
name string
in ContextTime
out []byte
}{
{
name: "Epoch",
in: ContextTime(time.Unix(0, 0)),
out: []byte(`"1970-01-01T00:00:00Z"`),
},
{
name: "WithTimeZone",
in: ContextTime(time.Unix(1629497889, 0).In(time.FixedZone("San Francisco (DST)", -7*60*60))),
out: []byte(`"2021-08-20T15:18:09-07:00"`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
out, err := tc.in.MarshalJSON()
if err != nil {
t.Errorf("ContextTime.MarshalJSON() = %+v , want %+q", err, tc.out)
return
}
if diff := cmp.Diff(tc.out, out); diff != "" {
t.Errorf("ContextTime.MarshalJSON got diff (-want +got): %s", diff)
return
}
})
}
}

func TestContextTimeUnmarshalJSON(t *testing.T) {
tests := []struct {
name string
in []byte
// ContextTime sets internal fields we don't care about -- easier to compare Unix times.
out int64
ok bool
}{
{
name: "RFC3339",
in: []byte(`"2021-08-20T22:35:40Z"`),
out: 1629498940,
ok: true,
},
{
name: "YYYY-MM-DD",
in: []byte(`"2021-08-20"`),
out: 1629417600,
ok: true,
},
// TODO should wrong quotes be accepted?
{
name: "RFC3339NoQuotes",
in: []byte(`2021-08-20T22:35:40Z`),
out: 1629498940,
ok: true,
},
{
name: "YYYY-MM-DDNoQuotes",
in: []byte(`2021-08-20`),
out: 1629417600,
ok: true,
},
{
name: "RFC3339OnlyOpeningQuote",
in: []byte(`"2021-08-20T22:35:40Z`),
out: 1629498940,
ok: true,
},
{
name: "YYYY-MM-DDOnlyOpeningQuote",
in: []byte(`"2021-08-20`),
out: 1629417600,
ok: true,
},
{
name: "RFC3339OnlyClosingQuote",
in: []byte(`2021-08-20T22:35:40Z"`),
out: 1629498940,
ok: true,
},
{
name: "YYYY-MM-DDOnlyClosingQuote",
in: []byte(`2021-08-20"`),
out: 1629417600,
ok: true,
},
{
name: "Invalid",
in: []byte("foobar"),
},
{
name: "BrokenRFC3339",
in: []byte(`"2021-08-2022:35:40Z"`),
},
{
name: "BrokenYYYY-MM-DD",
in: []byte(`"2021-13-20"`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ct := &ContextTime{}
err := ct.UnmarshalJSON(tc.in)
if tc.ok && err != nil {
t.Errorf("ContextTime.UnmarshalJSON(%+v) got err %+v, want %+v", tc.in, err, tc.out)
return
}
if !tc.ok && err == nil {
t.Errorf("ContextTime.UnmarshalJSON(%+v) got %+v, want err", tc.in, ct)
return
}
// ContextTime sets internal fields that we don't care about that makes cmp.Diff undesirable for comparison here.
gotUnixTime := time.Time(*ct).Unix()
if tc.ok && gotUnixTime != tc.out {
t.Errorf("ContextTime.UnmarshalJSON(%+v) got time %d, want %d", tc.in, gotUnixTime, tc.out)
return
}
})
}
}
47 changes: 0 additions & 47 deletions claat/types/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package types

import (
"bytes"
"time"

"github.com/googlecodelabs/tools/claat/nodes"
Expand All @@ -42,22 +41,6 @@ type Meta struct {
URL string `json:"url"` // Legacy ID; TODO: remove
}

// Context is an export context.
// It is defined in this package so that it can be used by both cli and a server.
type Context struct {
Env string `json:"environment"` // Current export environment
Format string `json:"format"` // Output format, e.g. "html"
Prefix string `json:"prefix,omitempty"` // Assets URL prefix for HTML-based formats
MainGA string `json:"mainga,omitempty"` // Global Google Analytics ID
Updated *ContextTime `json:"updated,omitempty"` // Last update timestamp
}

// ContextMeta is a composition of export context and meta data.
type ContextMeta struct {
Context
Meta
}

// Codelab is a top-level structure containing metadata and codelab steps.
type Codelab struct {
Meta
Expand Down Expand Up @@ -85,33 +68,3 @@ type Step struct {
Duration time.Duration // Duration
Content *nodes.ListNode // Root node of the step nodes tree
}

// ContextTime is codelab metadata timestamp.
// It can be of "YYYY-MM-DD" or RFC3339 formats but marshaling
// always uses RFC3339 format.
type ContextTime time.Time

// MarshalJSON implements Marshaler interface.
func (ct ContextTime) MarshalJSON() ([]byte, error) {
v := time.Time(ct).Format(time.RFC3339)
b := make([]byte, len(v)+2)
b[0] = '"'
b[len(b)-1] = '"'
copy(b[1:], v)
return b, nil
}

// UnmarshalJSON implements Unmarshaler interface.
// Accepted format is "YYYY-MM-DD" or RFC3339.
func (ct *ContextTime) UnmarshalJSON(b []byte) error {
b = bytes.Trim(b, `"`)
t, err := time.Parse(time.RFC3339, string(b))
if err != nil {
t, err = time.Parse("2006-01-02", string(b))
}
if err != nil {
return err
}
*ct = ContextTime(t)
return nil
}